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.setExpanded(parent, True)
1134 self.view.setCurrentIndex(child)
1141 n = self.model.rowCount(parent)
1145 for row in xrange(n):
1146 self.view.setExpanded(parent, True)
1147 child = self.model.index(row, 0, parent)
1148 child_call_time = child.internalPointer().call_time
1149 if child_call_time < time:
1151 elif child_call_time == time:
1152 self.view.setCurrentIndex(child)
1154 elif child_call_time > time:
1158 child = self.model.index(0, 0, parent)
1159 self.view.setExpanded(parent, True)
1160 self.view.setCurrentIndex(child)
1163 self.view.setExpanded(parent, True)
1164 self.view.setCurrentIndex(last_child)
1167 # ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1169 def ExecComm(db, thread_id, time):
1170 query = QSqlQuery(db)
1171 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1172 " FROM comm_threads"
1173 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1174 " WHERE comm_threads.thread_id = " + str(thread_id) +
1175 " ORDER BY comms.c_time, comms.id")
1180 first = query.value(0)
1181 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1182 last = query.value(0)
1183 if not(last is None):
1187 # Container for (x, y) data
1190 def __init__(self, x=0, y=0):
1195 return "XY({}, {})".format(str(self.x), str(self.y))
1197 # Container for sub-range data
1200 def __init__(self, lo=0, hi=0):
1205 return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1207 # Graph data region base class
1209 class GraphDataRegion(object):
1211 def __init__(self, key, title = "", ordinal = ""):
1214 self.ordinal = ordinal
1216 # Function to sort GraphDataRegion
1218 def GraphDataRegionOrdinal(data_region):
1219 return data_region.ordinal
1221 # Attributes for a graph region
1223 class GraphRegionAttribute():
1225 def __init__(self, colour):
1226 self.colour = colour
1228 # Switch graph data region represents a task
1230 class SwitchGraphDataRegion(GraphDataRegion):
1232 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1233 super(SwitchGraphDataRegion, self).__init__(key)
1235 self.title = str(pid) + " / " + str(tid) + " " + comm
1236 # Order graph legend within exec comm by pid / tid / time
1237 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1238 self.exec_comm_id = exec_comm_id
1242 self.thread_id = thread_id
1243 self.comm_id = comm_id
1247 class GraphDataPoint():
1249 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1256 self.hregion = hregion
1257 self.vregion = vregion
1259 # Graph data (single graph) base class
1261 class GraphData(object):
1263 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1264 self.collection = collection
1270 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1271 index = len(self.points)
1273 x = float(Decimal(x) - self.xbase)
1274 y = float(Decimal(y) - self.ybase)
1276 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1278 def XToData(self, x):
1279 return Decimal(x) + self.xbase
1281 def YToData(self, y):
1282 return Decimal(y) + self.ybase
1284 # Switch graph data (for one CPU)
1286 class SwitchGraphData(GraphData):
1288 def __init__(self, db, collection, cpu, xbase):
1289 super(SwitchGraphData, self).__init__(collection, xbase)
1292 self.title = "CPU " + str(cpu)
1293 self.SelectSwitches(db)
1295 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1296 query = QSqlQuery(db)
1297 QueryExec(query, "SELECT id, c_time"
1299 " WHERE c_thread_id = " + str(thread_id) +
1300 " AND exec_flag = " + self.collection.glb.dbref.TRUE +
1301 " AND c_time >= " + str(start_time) +
1302 " AND c_time <= " + str(end_time) +
1303 " ORDER BY c_time, id")
1305 comm_id = query.value(0)
1306 if comm_id == last_comm_id:
1308 time = query.value(1)
1309 hregion = self.HRegion(db, thread_id, comm_id, time)
1310 self.AddPoint(time, 1000, None, None, hregion)
1312 def SelectSwitches(self, db):
1315 last_thread_id = None
1316 query = QSqlQuery(db)
1317 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1318 " FROM context_switches"
1319 " WHERE machine_id = " + str(self.collection.machine_id) +
1320 " AND cpu = " + str(self.cpu) +
1321 " ORDER BY time, id")
1323 flags = int(query.value(5))
1325 # Schedule-out: detect and add exec's
1326 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1327 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1329 # Schedule-in: add data point
1330 if len(self.points) == 0:
1331 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1332 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1333 self.AddPoint(start_time, 1000, None, None, hregion)
1334 time = query.value(0)
1335 comm_id = query.value(4)
1336 thread_id = query.value(2)
1337 hregion = self.HRegion(db, thread_id, comm_id, time)
1338 self.AddPoint(time, 1000, None, None, hregion)
1340 last_comm_id = comm_id
1341 last_thread_id = thread_id
1343 def NewHRegion(self, db, key, thread_id, comm_id, time):
1344 exec_comm_id = ExecComm(db, thread_id, time)
1345 query = QSqlQuery(db)
1346 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1348 pid = query.value(0)
1349 tid = query.value(1)
1353 query = QSqlQuery(db)
1354 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1356 comm = query.value(0)
1359 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1361 def HRegion(self, db, thread_id, comm_id, time):
1362 key = str(thread_id) + ":" + str(comm_id)
1363 hregion = self.collection.LookupHRegion(key)
1365 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1366 self.collection.AddHRegion(key, hregion)
1369 # Graph data collection (multiple related graphs) base class
1371 class GraphDataCollection(object):
1373 def __init__(self, glb):
1377 self.xrangelo = None
1378 self.xrangehi = None
1379 self.yrangelo = None
1380 self.yrangehi = None
1383 def AddGraphData(self, data):
1384 self.data.append(data)
1386 def LookupHRegion(self, key):
1387 if key in self.hregions:
1388 return self.hregions[key]
1391 def AddHRegion(self, key, hregion):
1392 self.hregions[key] = hregion
1394 # Switch graph data collection (SwitchGraphData for each CPU)
1396 class SwitchGraphDataCollection(GraphDataCollection):
1398 def __init__(self, glb, db, machine_id):
1399 super(SwitchGraphDataCollection, self).__init__(glb)
1401 self.machine_id = machine_id
1402 self.cpus = self.SelectCPUs(db)
1404 self.xrangelo = glb.StartTime(machine_id)
1405 self.xrangehi = glb.FinishTime(machine_id)
1407 self.yrangelo = Decimal(0)
1408 self.yrangehi = Decimal(1000)
1410 for cpu in self.cpus:
1411 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1413 def SelectCPUs(self, db):
1415 query = QSqlQuery(db)
1416 QueryExec(query, "SELECT DISTINCT cpu"
1417 " FROM context_switches"
1418 " WHERE machine_id = " + str(self.machine_id))
1420 cpus.append(int(query.value(0)))
1423 # Switch graph data graphics item displays the graphed data
1425 class SwitchGraphDataGraphicsItem(QGraphicsItem):
1427 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1428 super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1431 self.graph_width = graph_width
1432 self.graph_height = graph_height
1434 self.event_handler = event_handler
1435 self.setAcceptHoverEvents(True)
1437 def boundingRect(self):
1438 return QRectF(0, 0, self.graph_width, self.graph_height)
1440 def PaintPoint(self, painter, last, x):
1441 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1442 if last.x < self.attrs.subrange.x.lo:
1443 x0 = self.attrs.subrange.x.lo
1446 if x > self.attrs.subrange.x.hi:
1447 x1 = self.attrs.subrange.x.hi
1450 x0 = self.attrs.XToPixel(x0)
1451 x1 = self.attrs.XToPixel(x1)
1453 y0 = self.attrs.YToPixel(last.y)
1455 colour = self.attrs.region_attributes[last.hregion.key].colour
1459 painter.setPen(colour)
1460 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1462 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1464 def paint(self, painter, option, widget):
1466 for point in self.data.points:
1467 self.PaintPoint(painter, last, point.x)
1468 if point.x > self.attrs.subrange.x.hi:
1471 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1473 def BinarySearchPoint(self, target):
1475 higher_pos = len(self.data.points)
1477 pos = int((lower_pos + higher_pos) / 2)
1478 val = self.data.points[pos].x
1483 if higher_pos <= lower_pos + 1:
1486 def XPixelToData(self, x):
1487 x = self.attrs.PixelToX(x)
1488 if x < self.data.points[0].x:
1493 pos = self.BinarySearchPoint(x)
1495 return (low, pos, self.data.XToData(x))
1497 def EventToData(self, event):
1498 no_data = (None,) * 4
1499 if len(self.data.points) < 1:
1504 low0, pos0, time_from = self.XPixelToData(x)
1505 low1, pos1, time_to = self.XPixelToData(x + 1)
1509 for i in xrange(pos0, pos1 + 1):
1510 hregion = self.data.points[i].hregion
1511 hregions.add(hregion)
1515 time = self.data.XToData(self.data.points[i].x)
1516 hregion_times.append((hregion, time))
1517 return (time_from, time_to, hregions, hregion_times)
1519 def hoverMoveEvent(self, event):
1520 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1521 if time_from is not None:
1522 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1524 def hoverLeaveEvent(self, event):
1525 self.event_handler.NoPointEvent()
1527 def mousePressEvent(self, event):
1528 if event.button() != Qt.RightButton:
1529 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1531 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1533 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1535 # X-axis graphics item
1537 class XAxisGraphicsItem(QGraphicsItem):
1539 def __init__(self, width, parent=None):
1540 super(XAxisGraphicsItem, self).__init__(parent)
1543 self.max_mark_sz = 4
1544 self.height = self.max_mark_sz + 1
1546 def boundingRect(self):
1547 return QRectF(0, 0, self.width, self.height)
1550 attrs = self.parentItem().attrs
1551 subrange = attrs.subrange.x
1552 t = subrange.hi - subrange.lo
1553 s = (3.0 * t) / self.width
1559 def PaintMarks(self, painter, at_y, lo, hi, step, i):
1560 attrs = self.parentItem().attrs
1563 xp = attrs.XToPixel(x)
1570 sz = self.max_mark_sz
1572 painter.drawLine(xp, at_y, xp, at_y + sz)
1576 def paint(self, painter, option, widget):
1577 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1578 painter.drawLine(0, 0, self.width - 1, 0)
1580 attrs = self.parentItem().attrs
1581 subrange = attrs.subrange.x
1583 x_offset = n - (subrange.lo % n)
1586 x = subrange.lo + x_offset
1588 self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1590 def ScaleDimensions(self):
1592 attrs = self.parentItem().attrs
1593 lo = attrs.subrange.x.lo
1594 hi = (n * 10.0) + lo
1595 width = attrs.XToPixel(hi)
1598 return (n, lo, hi, width)
1600 def PaintScale(self, painter, at_x, at_y):
1601 n, lo, hi, width = self.ScaleDimensions()
1604 painter.drawLine(at_x, at_y, at_x + width, at_y)
1605 self.PaintMarks(painter, at_y, lo, hi, n, 0)
1607 def ScaleWidth(self):
1608 n, lo, hi, width = self.ScaleDimensions()
1611 def ScaleHeight(self):
1614 def ScaleUnit(self):
1615 return self.Step() * 10
1617 # Scale graphics item base class
1619 class ScaleGraphicsItem(QGraphicsItem):
1621 def __init__(self, axis, parent=None):
1622 super(ScaleGraphicsItem, self).__init__(parent)
1625 def boundingRect(self):
1626 scale_width = self.axis.ScaleWidth()
1629 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1631 def paint(self, painter, option, widget):
1632 scale_width = self.axis.ScaleWidth()
1635 self.axis.PaintScale(painter, 0, 5)
1637 painter.drawText(QPointF(x, 10), self.Text())
1640 return self.axis.ScaleUnit()
1645 # Switch graph scale graphics item
1647 class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1649 def __init__(self, axis, parent=None):
1650 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1654 if unit >= 1000000000:
1655 unit = int(unit / 1000000000)
1657 elif unit >= 1000000:
1658 unit = int(unit / 1000000)
1661 unit = int(unit / 1000)
1666 return " = " + str(unit) + " " + us
1668 # Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1670 class SwitchGraphGraphicsItem(QGraphicsItem):
1672 def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1673 super(SwitchGraphGraphicsItem, self).__init__(parent)
1674 self.collection = collection
1677 self.event_handler = event_handler
1682 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1684 self.title_graphics.setPos(margin, margin)
1685 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1686 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1688 self.graph_origin_x = margin + title_width + margin
1689 self.graph_origin_y = graph_height + margin
1693 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1695 self.x_axis = XAxisGraphicsItem(graph_width, self)
1696 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1699 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1700 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1702 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1704 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1705 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1707 self.width = self.graph_origin_x + graph_width + margin
1708 self.height = self.graph_origin_y + margin
1710 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1711 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1713 if parent and 'EnableRubberBand' in dir(parent):
1714 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1716 def boundingRect(self):
1717 return QRectF(0, 0, self.width, self.height)
1719 def paint(self, painter, option, widget):
1722 def RBXToPixel(self, x):
1723 return self.attrs.PixelToX(x - self.graph_origin_x)
1725 def RBXRangeToPixel(self, x0, x1):
1726 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1728 def RBPixelToTime(self, x):
1729 if x < self.data.points[0].x:
1730 return self.data.XToData(0)
1731 return self.data.XToData(x)
1733 def RBEventTimes(self, x0, x1):
1734 x0, x1 = self.RBXRangeToPixel(x0, x1)
1735 time_from = self.RBPixelToTime(x0)
1736 time_to = self.RBPixelToTime(x1)
1737 return (time_from, time_to)
1739 def RBEvent(self, x0, x1):
1740 time_from, time_to = self.RBEventTimes(x0, x1)
1741 self.event_handler.RangeEvent(time_from, time_to)
1743 def RBMoveEvent(self, x0, x1):
1746 self.RBEvent(x0, x1)
1748 def RBReleaseEvent(self, x0, x1, selection_state):
1751 x0, x1 = self.RBXRangeToPixel(x0, x1)
1752 self.event_handler.SelectEvent(x0, x1, selection_state)
1754 # Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1756 class VerticalBracketGraphicsItem(QGraphicsItem):
1758 def __init__(self, parent=None):
1759 super(VerticalBracketGraphicsItem, self).__init__(parent)
1765 def SetSize(self, width, height):
1766 self.width = width + 1
1767 self.height = height + 1
1769 def boundingRect(self):
1770 return QRectF(0, 0, self.width, self.height)
1772 def paint(self, painter, option, widget):
1773 colour = QColor(255, 255, 0, 32)
1774 painter.fillRect(0, 0, self.width, self.height, colour)
1776 y1 = self.height - 1
1777 painter.drawLine(0, 0, x1, 0)
1778 painter.drawLine(0, 0, 0, 3)
1779 painter.drawLine(x1, 0, x1, 3)
1780 painter.drawLine(0, y1, x1, y1)
1781 painter.drawLine(0, y1, 0, y1 - 3)
1782 painter.drawLine(x1, y1, x1, y1 - 3)
1784 # Graphics item to contain graphs arranged vertically
1786 class VertcalGraphSetGraphicsItem(QGraphicsItem):
1788 def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1789 super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1791 self.collection = collection
1796 self.height = self.top
1798 self.rubber_band = None
1799 self.rb_enabled = False
1802 for data in collection.data:
1803 child = child_class(collection, data, attrs, event_handler, first, self)
1804 child.setPos(0, self.height + 1)
1805 rect = child.boundingRect()
1806 if rect.right() > self.width:
1807 self.width = rect.right()
1808 self.height = self.height + rect.bottom() + 1
1811 self.bracket = VerticalBracketGraphicsItem(self)
1813 def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1816 self.rb_enabled = True
1817 self.rb_in_view = False
1818 self.setAcceptedMouseButtons(Qt.LeftButton)
1821 self.rb_event_handler = rb_event_handler
1822 self.mousePressEvent = self.MousePressEvent
1823 self.mouseMoveEvent = self.MouseMoveEvent
1824 self.mouseReleaseEvent = self.MouseReleaseEvent
1826 def boundingRect(self):
1827 return QRectF(0, 0, self.width, self.height)
1829 def paint(self, painter, option, widget):
1832 def RubberBandParent(self):
1833 scene = self.scene()
1834 view = scene.views()[0]
1835 viewport = view.viewport()
1838 def RubberBandSetGeometry(self, rect):
1839 scene_rectf = self.mapRectToScene(QRectF(rect))
1840 scene = self.scene()
1841 view = scene.views()[0]
1842 poly = view.mapFromScene(scene_rectf)
1843 self.rubber_band.setGeometry(poly.boundingRect())
1845 def SetSelection(self, selection_state):
1846 if self.rubber_band:
1848 self.RubberBandSetGeometry(selection_state)
1849 self.rubber_band.show()
1851 self.rubber_band.hide()
1853 def SetBracket(self, rect):
1855 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1856 self.bracket.setPos(x, y)
1857 self.bracket.SetSize(width, height)
1862 def RubberBandX(self, event):
1863 x = event.pos().toPoint().x()
1866 elif x > self.rb_xhi:
1869 self.rb_in_view = True
1872 def RubberBandRect(self, x):
1873 if self.rb_origin.x() <= x:
1874 width = x - self.rb_origin.x()
1875 rect = QRect(self.rb_origin, QSize(width, self.height))
1877 width = self.rb_origin.x() - x
1878 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1879 rect = QRect(top_left, QSize(width, self.height))
1882 def MousePressEvent(self, event):
1883 self.rb_in_view = False
1884 x = self.RubberBandX(event)
1885 self.rb_origin = QPoint(x, self.top)
1886 if self.rubber_band is None:
1887 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1888 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1890 self.rubber_band.show()
1891 self.rb_event_handler.RBMoveEvent(x, x)
1893 self.rubber_band.hide()
1895 def MouseMoveEvent(self, event):
1896 x = self.RubberBandX(event)
1897 rect = self.RubberBandRect(x)
1898 self.RubberBandSetGeometry(rect)
1900 self.rubber_band.show()
1901 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1903 def MouseReleaseEvent(self, event):
1904 x = self.RubberBandX(event)
1906 selection_state = self.RubberBandRect(x)
1908 selection_state = None
1909 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1911 # Switch graph legend data model
1913 class SwitchGraphLegendModel(QAbstractTableModel):
1915 def __init__(self, collection, region_attributes, parent=None):
1916 super(SwitchGraphLegendModel, self).__init__(parent)
1918 self.region_attributes = region_attributes
1920 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1921 self.child_count = len(self.child_items)
1923 self.highlight_set = set()
1925 self.column_headers = ("pid", "tid", "comm")
1927 def rowCount(self, parent):
1928 return self.child_count
1930 def headerData(self, section, orientation, role):
1931 if role != Qt.DisplayRole:
1933 if orientation != Qt.Horizontal:
1935 return self.columnHeader(section)
1937 def index(self, row, column, parent):
1938 return self.createIndex(row, column, self.child_items[row])
1940 def columnCount(self, parent=None):
1941 return len(self.column_headers)
1943 def columnHeader(self, column):
1944 return self.column_headers[column]
1946 def data(self, index, role):
1947 if role == Qt.BackgroundRole:
1948 child = self.child_items[index.row()]
1949 if child in self.highlight_set:
1950 return self.region_attributes[child.key].colour
1952 if role == Qt.ForegroundRole:
1953 child = self.child_items[index.row()]
1954 if child in self.highlight_set:
1955 return QColor(255, 255, 255)
1956 return self.region_attributes[child.key].colour
1957 if role != Qt.DisplayRole:
1959 hregion = self.child_items[index.row()]
1960 col = index.column()
1969 def SetHighlight(self, row, set_highlight):
1970 child = self.child_items[row]
1971 top_left = self.createIndex(row, 0, child)
1972 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1973 self.dataChanged.emit(top_left, bottom_right)
1975 def Highlight(self, highlight_set):
1976 for row in xrange(self.child_count):
1977 child = self.child_items[row]
1978 if child in self.highlight_set:
1979 if child not in highlight_set:
1980 self.SetHighlight(row, False)
1981 elif child in highlight_set:
1982 self.SetHighlight(row, True)
1983 self.highlight_set = highlight_set
1985 # Switch graph legend is a table
1987 class SwitchGraphLegend(QWidget):
1989 def __init__(self, collection, region_attributes, parent=None):
1990 super(SwitchGraphLegend, self).__init__(parent)
1992 self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1994 self.model = QSortFilterProxyModel()
1995 self.model.setSourceModel(self.data_model)
1997 self.view = QTableView()
1998 self.view.setModel(self.model)
1999 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2000 self.view.verticalHeader().setVisible(False)
2001 self.view.sortByColumn(-1, Qt.AscendingOrder)
2002 self.view.setSortingEnabled(True)
2003 self.view.resizeColumnsToContents()
2004 self.view.resizeRowsToContents()
2006 self.vbox = VBoxLayout(self.view)
2007 self.setLayout(self.vbox)
2009 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2010 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2011 self.saved_size = sz1
2013 def resizeEvent(self, event):
2014 self.saved_size = self.size().width()
2015 super(SwitchGraphLegend, self).resizeEvent(event)
2017 def Highlight(self, highlight_set):
2018 self.data_model.Highlight(highlight_set)
2021 def changeEvent(self, event):
2022 if event.type() == QEvent.FontChange:
2023 self.view.resizeRowsToContents()
2024 self.view.resizeColumnsToContents()
2025 # Need to resize rows again after column resize
2026 self.view.resizeRowsToContents()
2027 super(SwitchGraphLegend, self).changeEvent(event)
2029 # Random colour generation
2031 def RGBColourTooLight(r, g, b):
2036 if r <= 180 and g <= 180:
2042 def GenerateColours(x):
2044 for i in xrange(1, x):
2045 cs.append(int((255.0 / i) + 0.5))
2050 # Exclude black and colours that look too light against a white background
2051 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2053 colours.append(QColor(r, g, b))
2056 def GenerateNColours(n):
2057 for x in xrange(2, n + 2):
2058 colours = GenerateColours(x)
2059 if len(colours) >= n:
2063 def GenerateNRandomColours(n, seed):
2064 colours = GenerateNColours(n)
2066 random.shuffle(colours)
2069 # Graph attributes, in particular the scale and subrange that change when zooming
2071 class GraphAttributes():
2073 def __init__(self, scale, subrange, region_attributes, dp):
2075 self.subrange = subrange
2076 self.region_attributes = region_attributes
2077 # Rounding avoids errors due to finite floating point precision
2078 self.dp = dp # data decimal places
2081 def XToPixel(self, x):
2082 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2084 def YToPixel(self, y):
2085 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2087 def PixelToXRounded(self, px):
2088 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2090 def PixelToYRounded(self, py):
2091 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2093 def PixelToX(self, px):
2094 x = self.PixelToXRounded(px)
2096 rt = self.XToPixel(x)
2101 def PixelToY(self, py):
2102 y = self.PixelToYRounded(py)
2104 rt = self.YToPixel(y)
2109 def ToPDP(self, dp, scale):
2110 # Calculate pixel decimal places:
2111 # (10 ** dp) is the minimum delta in the data
2112 # scale it to get the minimum delta in pixels
2113 # log10 gives the number of decimals places negatively
2114 # subtrace 1 to divide by 10
2115 # round to the lower negative number
2116 # change the sign to get the number of decimals positively
2117 x = math.log10((10 ** dp) * scale)
2120 x = -int(math.floor(x) - 0.1)
2126 x = self.ToPDP(self.dp.x, self.scale.x)
2127 y = self.ToPDP(self.dp.y, self.scale.y)
2128 self.pdp = XY(x, y) # pixel decimal places
2130 # Switch graph splitter which divides the CPU graphs from the legend
2132 class SwitchGraphSplitter(QSplitter):
2134 def __init__(self, parent=None):
2135 super(SwitchGraphSplitter, self).__init__(parent)
2137 self.first_time = False
2139 def resizeEvent(self, ev):
2141 self.first_time = False
2142 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2143 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2144 sz0 = self.size().width() - self.handleWidth() - sz1
2145 self.setSizes([sz0, sz1])
2146 elif not(self.widget(1).saved_size is None):
2147 sz1 = self.widget(1).saved_size
2148 sz0 = self.size().width() - self.handleWidth() - sz1
2149 self.setSizes([sz0, sz1])
2150 super(SwitchGraphSplitter, self).resizeEvent(ev)
2152 # Graph widget base class
2154 class GraphWidget(QWidget):
2156 graph_title_changed = Signal(object)
2158 def __init__(self, parent=None):
2159 super(GraphWidget, self).__init__(parent)
2161 def GraphTitleChanged(self, title):
2162 self.graph_title_changed.emit(title)
2167 # Display time in s, ms, us or ns
2171 if val >= 1000000000:
2172 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2174 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2176 return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2177 return "{} ns".format(val.quantize(Decimal("1")))
2179 # Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2181 class SwitchGraphWidget(GraphWidget):
2183 def __init__(self, glb, collection, parent=None):
2184 super(SwitchGraphWidget, self).__init__(parent)
2187 self.collection = collection
2189 self.back_state = []
2190 self.forward_state = []
2191 self.selection_state = (None, None)
2192 self.fwd_rect = None
2193 self.start_time = self.glb.StartTime(collection.machine_id)
2196 hregions = collection.hregions.values()
2197 colours = GenerateNRandomColours(len(hregions), 1013)
2198 region_attributes = {}
2199 for hregion in hregions:
2200 if hregion.pid == 0 and hregion.tid == 0:
2201 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2203 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2206 # Default to entire range
2207 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2208 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2209 subrange = XY(xsubrange, ysubrange)
2211 scale = self.GetScaleForRange(subrange)
2213 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2215 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2217 self.scene = QGraphicsScene()
2218 self.scene.addItem(self.item)
2220 self.view = QGraphicsView(self.scene)
2221 self.view.centerOn(0, 0)
2222 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2224 self.legend = SwitchGraphLegend(collection, region_attributes)
2226 self.splitter = SwitchGraphSplitter()
2227 self.splitter.addWidget(self.view)
2228 self.splitter.addWidget(self.legend)
2230 self.point_label = QLabel("")
2231 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2233 self.back_button = QToolButton()
2234 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2235 self.back_button.setDisabled(True)
2236 self.back_button.released.connect(lambda: self.Back())
2238 self.forward_button = QToolButton()
2239 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2240 self.forward_button.setDisabled(True)
2241 self.forward_button.released.connect(lambda: self.Forward())
2243 self.zoom_button = QToolButton()
2244 self.zoom_button.setText("Zoom")
2245 self.zoom_button.setDisabled(True)
2246 self.zoom_button.released.connect(lambda: self.Zoom())
2248 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2250 self.vbox = VBoxLayout(self.splitter, self.hbox)
2252 self.setLayout(self.vbox)
2254 def GetScaleForRangeX(self, xsubrange):
2255 # Default graph 1000 pixels wide
2257 r = xsubrange.hi - xsubrange.lo
2260 def GetScaleForRangeY(self, ysubrange):
2261 # Default graph 50 pixels high
2263 r = ysubrange.hi - ysubrange.lo
2266 def GetScaleForRange(self, subrange):
2267 # Default graph 1000 pixels wide, 50 pixels high
2268 xscale = self.GetScaleForRangeX(subrange.x)
2269 yscale = self.GetScaleForRangeY(subrange.y)
2270 return XY(xscale, yscale)
2272 def PointEvent(self, cpu, time_from, time_to, hregions):
2273 text = "CPU: " + str(cpu)
2274 time_from = time_from.quantize(Decimal(1))
2275 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2276 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2277 self.point_label.setText(text)
2278 self.legend.Highlight(hregions)
2280 def RightClickEvent(self, cpu, hregion_times, pos):
2281 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2283 menu = QMenu(self.view)
2284 for hregion, time in hregion_times:
2285 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2286 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2287 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2290 def RightClickSelect(self, args):
2291 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2293 def NoPointEvent(self):
2294 self.point_label.setText("")
2295 self.legend.Highlight({})
2297 def RangeEvent(self, time_from, time_to):
2298 time_from = time_from.quantize(Decimal(1))
2299 time_to = time_to.quantize(Decimal(1))
2300 if time_to <= time_from:
2301 self.point_label.setText("")
2303 rel_time_from = time_from - self.start_time
2304 rel_time_to = time_to - self.start_time
2305 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2306 text = text + " duration: " + ToTimeStr(time_to - time_from)
2307 self.point_label.setText(text)
2309 def BackState(self):
2310 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2312 def PushBackState(self):
2313 state = copy.deepcopy(self.BackState())
2314 self.back_state.append(state)
2315 self.back_button.setEnabled(True)
2317 def PopBackState(self):
2318 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2320 if not self.back_state:
2321 self.back_button.setDisabled(True)
2323 def PushForwardState(self):
2324 state = copy.deepcopy(self.BackState())
2325 self.forward_state.append(state)
2326 self.forward_button.setEnabled(True)
2328 def PopForwardState(self):
2329 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2331 if not self.forward_state:
2332 self.forward_button.setDisabled(True)
2335 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2336 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2337 rel_time_from = time_from - self.start_time
2338 rel_time_to = time_to - self.start_time
2339 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2340 title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2344 selected_subrange, selection_state = self.selection_state
2345 self.item.SetSelection(selection_state)
2346 self.item.SetBracket(self.fwd_rect)
2347 self.zoom_button.setDisabled(selected_subrange is None)
2348 self.GraphTitleChanged(self.Title())
2349 self.item.update(self.item.boundingRect())
2352 if not self.back_state:
2354 self.PushForwardState()
2359 if not self.forward_state:
2361 self.PushBackState()
2362 self.PopForwardState()
2365 def SelectEvent(self, x0, x1, selection_state):
2366 if selection_state is None:
2367 selected_subrange = None
2371 selected_subrange = Subrange(x0, x1)
2372 self.selection_state = (selected_subrange, selection_state)
2373 self.zoom_button.setDisabled(selected_subrange is None)
2376 selected_subrange, selection_state = self.selection_state
2377 if selected_subrange is None:
2379 self.fwd_rect = selection_state
2380 self.item.SetSelection(None)
2381 self.PushBackState()
2382 self.attrs.subrange.x = selected_subrange
2383 self.forward_state = []
2384 self.forward_button.setDisabled(True)
2385 self.selection_state = (None, None)
2386 self.fwd_rect = None
2387 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2391 # Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2393 class SlowInitClass():
2395 def __init__(self, glb, title, init_fn):
2396 self.init_fn = init_fn
2400 self.msg_box = QMessageBox(glb.mainwindow)
2401 self.msg_box.setText("Initializing " + title + ". Please wait.")
2402 self.msg_box.setWindowTitle("Initializing " + title)
2403 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2405 self.init_thread = Thread(self.ThreadFn, glb)
2406 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2408 self.init_thread.start()
2411 self.msg_box.done(0)
2413 def ThreadFn(self, glb):
2414 conn_name = "SlowInitClass" + str(os.getpid())
2415 db, dbname = glb.dbref.Open(conn_name)
2416 self.result = self.init_fn(db)
2421 while not self.done:
2422 self.msg_box.exec_()
2423 self.init_thread.wait()
2426 def SlowInit(glb, title, init_fn):
2427 init = SlowInitClass(glb, title, init_fn)
2428 return init.Result()
2430 # Time chart by CPU window
2432 class TimeChartByCPUWindow(QMdiSubWindow):
2434 def __init__(self, glb, parent=None):
2435 super(TimeChartByCPUWindow, self).__init__(parent)
2438 self.machine_id = glb.HostMachineId()
2439 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2441 collection = LookupModel(self.collection_name)
2442 if collection is None:
2443 collection = SlowInit(glb, "Time Chart", self.Init)
2445 self.widget = SwitchGraphWidget(glb, collection, self)
2446 self.view = self.widget
2448 self.base_title = "Time Chart by CPU"
2449 self.setWindowTitle(self.base_title + self.widget.Title())
2450 self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2452 self.setWidget(self.widget)
2454 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2457 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2459 def GraphTitleChanged(self, title):
2460 self.setWindowTitle(self.base_title + " : " + title)
2462 # Child data item finder
2464 class ChildDataItemFinder():
2466 def __init__(self, root):
2468 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2472 def FindSelect(self):
2475 pattern = re.compile(self.value)
2476 for child in self.root.child_items:
2477 for column_data in child.data:
2478 if re.search(pattern, str(column_data)) is not None:
2479 self.rows.append(child.row)
2482 for child in self.root.child_items:
2483 for column_data in child.data:
2484 if self.value in str(column_data):
2485 self.rows.append(child.row)
2488 def FindValue(self):
2490 if self.last_value != self.value or self.pattern != self.last_pattern:
2492 if not len(self.rows):
2494 return self.rows[self.pos]
2496 def FindThread(self):
2497 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2498 row = self.FindValue()
2499 elif len(self.rows):
2500 if self.direction > 0:
2502 if self.pos >= len(self.rows):
2507 self.pos = len(self.rows) - 1
2508 row = self.rows[self.pos]
2513 def Find(self, value, direction, pattern, context, callback):
2514 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2515 # Use a thread so the UI is not blocked
2516 thread = Thread(self.FindThread)
2517 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2520 def FindDone(self, thread, callback, row):
2523 # Number of database records to fetch in one go
2525 glb_chunk_sz = 10000
2527 # Background process for SQL data fetcher
2529 class SQLFetcherProcess():
2531 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2532 # Need a unique connection name
2533 conn_name = "SQLFetcher" + str(os.getpid())
2534 self.db, dbname = dbref.Open(conn_name)
2536 self.buffer = buffer
2539 self.fetch_count = fetch_count
2540 self.fetching_done = fetching_done
2541 self.process_target = process_target
2542 self.wait_event = wait_event
2543 self.fetched_event = fetched_event
2545 self.query = QSqlQuery(self.db)
2546 self.query_limit = 0 if "$$last_id$$" in sql else 2
2550 self.local_head = self.head.value
2551 self.local_tail = self.tail.value
2554 if self.query_limit:
2555 if self.query_limit == 1:
2557 self.query_limit -= 1
2558 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2559 QueryExec(self.query, stmt)
2562 if not self.query.next():
2564 if not self.query.next():
2566 self.last_id = self.query.value(0)
2567 return self.prep(self.query)
2569 def WaitForTarget(self):
2571 self.wait_event.clear()
2572 target = self.process_target.value
2573 if target > self.fetched or target < 0:
2575 self.wait_event.wait()
2578 def HasSpace(self, sz):
2579 if self.local_tail <= self.local_head:
2580 space = len(self.buffer) - self.local_head
2583 if space >= glb_nsz:
2584 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2585 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2586 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2588 if self.local_tail - self.local_head > sz:
2592 def WaitForSpace(self, sz):
2593 if self.HasSpace(sz):
2596 self.wait_event.clear()
2597 self.local_tail = self.tail.value
2598 if self.HasSpace(sz):
2600 self.wait_event.wait()
2602 def AddToBuffer(self, obj):
2603 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2605 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2607 self.WaitForSpace(sz)
2608 pos = self.local_head
2609 self.buffer[pos : pos + len(nd)] = nd
2610 self.buffer[pos + glb_nsz : pos + sz] = d
2611 self.local_head += sz
2613 def FetchBatch(self, batch_size):
2615 while batch_size > fetched:
2620 self.AddToBuffer(obj)
2623 self.fetched += fetched
2624 with self.fetch_count.get_lock():
2625 self.fetch_count.value += fetched
2626 self.head.value = self.local_head
2627 self.fetched_event.set()
2631 target = self.WaitForTarget()
2634 batch_size = min(glb_chunk_sz, target - self.fetched)
2635 self.FetchBatch(batch_size)
2636 self.fetching_done.value = True
2637 self.fetched_event.set()
2639 def SQLFetcherFn(*x):
2640 process = SQLFetcherProcess(*x)
2645 class SQLFetcher(QObject):
2647 done = Signal(object)
2649 def __init__(self, glb, sql, prep, process_data, parent=None):
2650 super(SQLFetcher, self).__init__(parent)
2651 self.process_data = process_data
2654 self.last_target = 0
2656 self.buffer_size = 16 * 1024 * 1024
2657 self.buffer = Array(c_char, self.buffer_size, lock=False)
2658 self.head = Value(c_longlong)
2659 self.tail = Value(c_longlong)
2661 self.fetch_count = Value(c_longlong)
2662 self.fetching_done = Value(c_bool)
2664 self.process_target = Value(c_longlong)
2665 self.wait_event = Event()
2666 self.fetched_event = Event()
2667 glb.AddInstanceToShutdownOnExit(self)
2668 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))
2669 self.process.start()
2670 self.thread = Thread(self.Thread)
2671 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2675 # Tell the thread and process to exit
2676 self.process_target.value = -1
2677 self.wait_event.set()
2679 self.fetching_done.value = True
2680 self.fetched_event.set()
2686 self.fetched_event.clear()
2687 fetch_count = self.fetch_count.value
2688 if fetch_count != self.last_count:
2690 if self.fetching_done.value:
2693 self.fetched_event.wait()
2694 count = fetch_count - self.last_count
2695 self.last_count = fetch_count
2696 self.fetched += count
2699 def Fetch(self, nr):
2701 # -1 inidcates there are no more
2703 result = self.fetched
2704 extra = result + nr - self.target
2706 self.target += extra
2707 # process_target < 0 indicates shutting down
2708 if self.process_target.value >= 0:
2709 self.process_target.value = self.target
2710 self.wait_event.set()
2713 def RemoveFromBuffer(self):
2714 pos = self.local_tail
2715 if len(self.buffer) - pos < glb_nsz:
2717 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2720 n = pickle.loads(self.buffer[0 : glb_nsz])
2722 obj = pickle.loads(self.buffer[pos : pos + n])
2723 self.local_tail = pos + n
2726 def ProcessData(self, count):
2727 for i in xrange(count):
2728 obj = self.RemoveFromBuffer()
2729 self.process_data(obj)
2730 self.tail.value = self.local_tail
2731 self.wait_event.set()
2732 self.done.emit(count)
2734 # Fetch more records bar
2736 class FetchMoreRecordsBar():
2738 def __init__(self, model, parent):
2741 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2742 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2744 self.fetch_count = QSpinBox()
2745 self.fetch_count.setRange(1, 1000000)
2746 self.fetch_count.setValue(10)
2747 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2749 self.fetch = QPushButton("Go!")
2750 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2751 self.fetch.released.connect(self.FetchMoreRecords)
2753 self.progress = QProgressBar()
2754 self.progress.setRange(0, 100)
2755 self.progress.hide()
2757 self.done_label = QLabel("All records fetched")
2758 self.done_label.hide()
2760 self.spacer = QLabel("")
2762 self.close_button = QToolButton()
2763 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2764 self.close_button.released.connect(self.Deactivate)
2766 self.hbox = QHBoxLayout()
2767 self.hbox.setContentsMargins(0, 0, 0, 0)
2769 self.hbox.addWidget(self.label)
2770 self.hbox.addWidget(self.fetch_count)
2771 self.hbox.addWidget(self.fetch)
2772 self.hbox.addWidget(self.spacer)
2773 self.hbox.addWidget(self.progress)
2774 self.hbox.addWidget(self.done_label)
2775 self.hbox.addWidget(self.close_button)
2777 self.bar = QWidget()
2778 self.bar.setLayout(self.hbox)
2781 self.in_progress = False
2782 self.model.progress.connect(self.Progress)
2786 if not model.HasMoreRecords():
2794 self.fetch.setFocus()
2796 def Deactivate(self):
2799 def Enable(self, enable):
2800 self.fetch.setEnabled(enable)
2801 self.fetch_count.setEnabled(enable)
2807 self.progress.show()
2810 self.in_progress = False
2812 self.progress.hide()
2817 return self.fetch_count.value() * glb_chunk_sz
2823 self.fetch_count.hide()
2826 self.done_label.show()
2828 def Progress(self, count):
2829 if self.in_progress:
2831 percent = ((count - self.start) * 100) / self.Target()
2835 self.progress.setValue(percent)
2837 # Count value of zero means no more records
2840 def FetchMoreRecords(self):
2843 self.progress.setValue(0)
2845 self.in_progress = True
2846 self.start = self.model.FetchMoreRecords(self.Target())
2848 # Brance data model level two item
2850 class BranchLevelTwoItem():
2852 def __init__(self, row, col, text, parent_item):
2854 self.parent_item = parent_item
2855 self.data = [""] * (col + 1)
2856 self.data[col] = text
2859 def getParentItem(self):
2860 return self.parent_item
2865 def childCount(self):
2868 def hasChildren(self):
2871 def getData(self, column):
2872 return self.data[column]
2874 # Brance data model level one item
2876 class BranchLevelOneItem():
2878 def __init__(self, glb, row, data, parent_item):
2881 self.parent_item = parent_item
2882 self.child_count = 0
2883 self.child_items = []
2884 self.data = data[1:]
2887 self.query_done = False
2888 self.br_col = len(self.data) - 1
2890 def getChildItem(self, row):
2891 return self.child_items[row]
2893 def getParentItem(self):
2894 return self.parent_item
2900 self.query_done = True
2902 if not self.glb.have_disassembler:
2905 query = QSqlQuery(self.glb.db)
2907 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2909 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2910 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2911 " WHERE samples.id = " + str(self.dbid))
2912 if not query.next():
2914 cpu = query.value(0)
2915 dso = query.value(1)
2916 sym = query.value(2)
2917 if dso == 0 or sym == 0:
2919 off = query.value(3)
2920 short_name = query.value(4)
2921 long_name = query.value(5)
2922 build_id = query.value(6)
2923 sym_start = query.value(7)
2926 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2928 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2929 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2930 " ORDER BY samples.id"
2932 if not query.next():
2934 if query.value(0) != dso:
2935 # Cannot disassemble from one dso to another
2937 bsym = query.value(1)
2938 boff = query.value(2)
2939 bsym_start = query.value(3)
2942 tot = bsym_start + boff + 1 - sym_start - off
2943 if tot <= 0 or tot > 16384:
2946 inst = self.glb.disassembler.Instruction()
2947 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2950 mode = 0 if Is64Bit(f) else 1
2951 self.glb.disassembler.SetMode(inst, mode)
2954 buf = create_string_buffer(tot + 16)
2955 f.seek(sym_start + off)
2956 buf.value = f.read(buf_sz)
2957 buf_ptr = addressof(buf)
2960 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2962 byte_str = tohex(ip).rjust(16)
2963 for k in xrange(cnt):
2964 byte_str += " %02x" % ord(buf[i])
2969 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2970 self.child_count += 1
2978 def childCount(self):
2979 if not self.query_done:
2981 if not self.child_count:
2983 return self.child_count
2985 def hasChildren(self):
2986 if not self.query_done:
2988 return self.child_count > 0
2990 def getData(self, column):
2991 return self.data[column]
2993 # Brance data model root item
2995 class BranchRootItem():
2998 self.child_count = 0
2999 self.child_items = []
3002 def getChildItem(self, row):
3003 return self.child_items[row]
3005 def getParentItem(self):
3011 def childCount(self):
3012 return self.child_count
3014 def hasChildren(self):
3015 return self.child_count > 0
3017 def getData(self, column):
3020 # Calculate instructions per cycle
3022 def CalcIPC(cyc_cnt, insn_cnt):
3023 if cyc_cnt and insn_cnt:
3024 ipc = Decimal(float(insn_cnt) / cyc_cnt)
3025 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3030 # Branch data preparation
3032 def BranchDataPrepBr(query, data):
3033 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3034 " (" + dsoname(query.value(11)) + ")" + " -> " +
3035 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3036 " (" + dsoname(query.value(15)) + ")")
3038 def BranchDataPrepIPC(query, data):
3039 insn_cnt = query.value(16)
3040 cyc_cnt = query.value(17)
3041 ipc = CalcIPC(cyc_cnt, insn_cnt)
3042 data.append(insn_cnt)
3043 data.append(cyc_cnt)
3046 def BranchDataPrep(query):
3048 for i in xrange(0, 8):
3049 data.append(query.value(i))
3050 BranchDataPrepBr(query, data)
3053 def BranchDataPrepWA(query):
3055 data.append(query.value(0))
3056 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3057 data.append("{:>19}".format(query.value(1)))
3058 for i in xrange(2, 8):
3059 data.append(query.value(i))
3060 BranchDataPrepBr(query, data)
3063 def BranchDataWithIPCPrep(query):
3065 for i in xrange(0, 8):
3066 data.append(query.value(i))
3067 BranchDataPrepIPC(query, data)
3068 BranchDataPrepBr(query, data)
3071 def BranchDataWithIPCPrepWA(query):
3073 data.append(query.value(0))
3074 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3075 data.append("{:>19}".format(query.value(1)))
3076 for i in xrange(2, 8):
3077 data.append(query.value(i))
3078 BranchDataPrepIPC(query, data)
3079 BranchDataPrepBr(query, data)
3084 class BranchModel(TreeModel):
3086 progress = Signal(object)
3088 def __init__(self, glb, event_id, where_clause, parent=None):
3089 super(BranchModel, self).__init__(glb, None, parent)
3090 self.event_id = event_id
3093 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3095 select_ipc = ", insn_count, cyc_count"
3096 prep_fn = BranchDataWithIPCPrep
3097 prep_wa_fn = BranchDataWithIPCPrepWA
3100 prep_fn = BranchDataPrep
3101 prep_wa_fn = BranchDataPrepWA
3102 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3103 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3104 " ip, symbols.name, sym_offset, dsos.short_name,"
3105 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3108 " INNER JOIN comms ON comm_id = comms.id"
3109 " INNER JOIN threads ON thread_id = threads.id"
3110 " INNER JOIN branch_types ON branch_type = branch_types.id"
3111 " INNER JOIN symbols ON symbol_id = symbols.id"
3112 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3113 " INNER JOIN dsos ON samples.dso_id = dsos.id"
3114 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3115 " WHERE samples.id > $$last_id$$" + where_clause +
3116 " AND evsel_id = " + str(self.event_id) +
3117 " ORDER BY samples.id"
3118 " LIMIT " + str(glb_chunk_sz))
3119 if pyside_version_1 and sys.version_info[0] == 3:
3123 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3124 self.fetcher.done.connect(self.Update)
3125 self.fetcher.Fetch(glb_chunk_sz)
3128 return BranchRootItem()
3130 def columnCount(self, parent=None):
3136 def columnHeader(self, column):
3138 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3140 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3142 def columnFont(self, column):
3147 if column != br_col:
3149 return QFont("Monospace")
3151 def DisplayData(self, item, index):
3153 self.FetchIfNeeded(item.row)
3154 return item.getData(index.column())
3156 def AddSample(self, data):
3157 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3158 self.root.child_items.append(child)
3161 def Update(self, fetched):
3164 self.progress.emit(0)
3165 child_count = self.root.child_count
3166 count = self.populated - child_count
3168 parent = QModelIndex()
3169 self.beginInsertRows(parent, child_count, child_count + count - 1)
3170 self.insertRows(child_count, count, parent)
3171 self.root.child_count += count
3172 self.endInsertRows()
3173 self.progress.emit(self.root.child_count)
3175 def FetchMoreRecords(self, count):
3176 current = self.root.child_count
3178 self.fetcher.Fetch(count)
3180 self.progress.emit(0)
3183 def HasMoreRecords(self):
3190 def __init__(self, name = "", where_clause = "", limit = ""):
3192 self.where_clause = where_clause
3196 return str(self.where_clause + ";" + self.limit)
3200 class BranchWindow(QMdiSubWindow):
3202 def __init__(self, glb, event_id, report_vars, parent=None):
3203 super(BranchWindow, self).__init__(parent)
3205 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
3207 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3209 self.view = QTreeView()
3210 self.view.setUniformRowHeights(True)
3211 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3212 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3213 self.view.setModel(self.model)
3215 self.ResizeColumnsToContents()
3217 self.context_menu = TreeContextMenu(self.view)
3219 self.find_bar = FindBar(self, self, True)
3221 self.finder = ChildDataItemFinder(self.model.root)
3223 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3225 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3227 self.setWidget(self.vbox.Widget())
3229 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3231 def ResizeColumnToContents(self, column, n):
3232 # Using the view's resizeColumnToContents() here is extrememly slow
3233 # so implement a crude alternative
3234 mm = "MM" if column else "MMMM"
3235 font = self.view.font()
3236 metrics = QFontMetrics(font)
3238 for row in xrange(n):
3239 val = self.model.root.child_items[row].data[column]
3240 len = metrics.width(str(val) + mm)
3241 max = len if len > max else max
3242 val = self.model.columnHeader(column)
3243 len = metrics.width(str(val) + mm)
3244 max = len if len > max else max
3245 self.view.setColumnWidth(column, max)
3247 def ResizeColumnsToContents(self):
3248 n = min(self.model.root.child_count, 100)
3250 # No data yet, so connect a signal to notify when there is
3251 self.model.rowsInserted.connect(self.UpdateColumnWidths)
3253 columns = self.model.columnCount()
3254 for i in xrange(columns):
3255 self.ResizeColumnToContents(i, n)
3257 def UpdateColumnWidths(self, *x):
3258 # This only needs to be done once, so disconnect the signal now
3259 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3260 self.ResizeColumnsToContents()
3262 def Find(self, value, direction, pattern, context):
3263 self.view.setFocus()
3264 self.find_bar.Busy()
3265 self.finder.Find(value, direction, pattern, context, self.FindDone)
3267 def FindDone(self, row):
3268 self.find_bar.Idle()
3270 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3272 self.find_bar.NotFound()
3274 # Line edit data item
3276 class LineEditDataItem(object):
3278 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3281 self.placeholder_text = placeholder_text
3282 self.parent = parent
3285 self.value = default
3287 self.widget = QLineEdit(default)
3288 self.widget.editingFinished.connect(self.Validate)
3289 self.widget.textChanged.connect(self.Invalidate)
3292 self.validated = True
3294 if placeholder_text:
3295 self.widget.setPlaceholderText(placeholder_text)
3297 def TurnTextRed(self):
3299 palette = QPalette()
3300 palette.setColor(QPalette.Text,Qt.red)
3301 self.widget.setPalette(palette)
3304 def TurnTextNormal(self):
3306 palette = QPalette()
3307 self.widget.setPalette(palette)
3310 def InvalidValue(self, value):
3313 self.error = self.label + " invalid value '" + value + "'"
3314 self.parent.ShowMessage(self.error)
3316 def Invalidate(self):
3317 self.validated = False
3319 def DoValidate(self, input_string):
3320 self.value = input_string.strip()
3323 self.validated = True
3325 self.TurnTextNormal()
3326 self.parent.ClearMessage()
3327 input_string = self.widget.text()
3328 if not len(input_string.strip()):
3331 self.DoValidate(input_string)
3334 if not self.validated:
3337 self.parent.ShowMessage(self.error)
3341 def IsNumber(self, value):
3346 return str(x) == value
3348 # Non-negative integer ranges dialog data item
3350 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3352 def __init__(self, glb, label, placeholder_text, column_name, parent):
3353 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3355 self.column_name = column_name
3357 def DoValidate(self, input_string):
3360 for value in [x.strip() for x in input_string.split(",")]:
3362 vrange = value.split("-")
3363 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3364 return self.InvalidValue(value)
3365 ranges.append(vrange)
3367 if not self.IsNumber(value):
3368 return self.InvalidValue(value)
3369 singles.append(value)
3370 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3372 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3373 self.value = " OR ".join(ranges)
3375 # Positive integer dialog data item
3377 class PositiveIntegerDataItem(LineEditDataItem):
3379 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3380 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3382 def DoValidate(self, input_string):
3383 if not self.IsNumber(input_string.strip()):
3384 return self.InvalidValue(input_string)
3385 value = int(input_string.strip())
3387 return self.InvalidValue(input_string)
3388 self.value = str(value)
3390 # Dialog data item converted and validated using a SQL table
3392 class SQLTableDataItem(LineEditDataItem):
3394 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3395 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3397 self.table_name = table_name
3398 self.match_column = match_column
3399 self.column_name1 = column_name1
3400 self.column_name2 = column_name2
3402 def ValueToIds(self, value):
3404 query = QSqlQuery(self.glb.db)
3405 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3406 ret = query.exec_(stmt)
3409 ids.append(str(query.value(0)))
3412 def DoValidate(self, input_string):
3414 for value in [x.strip() for x in input_string.split(",")]:
3415 ids = self.ValueToIds(value)
3419 return self.InvalidValue(value)
3420 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3421 if self.column_name2:
3422 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3424 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
3426 class SampleTimeRangesDataItem(LineEditDataItem):
3428 def __init__(self, glb, label, placeholder_text, column_name, parent):
3429 self.column_name = column_name
3433 self.last_time = 2 ** 64
3435 query = QSqlQuery(glb.db)
3436 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3438 self.last_id = int(query.value(0))
3439 self.first_time = int(glb.HostStartTime())
3440 self.last_time = int(glb.HostFinishTime())
3441 if placeholder_text:
3442 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3444 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3446 def IdBetween(self, query, lower_id, higher_id, order):
3447 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3449 return True, int(query.value(0))
3453 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3454 query = QSqlQuery(self.glb.db)
3456 next_id = int((lower_id + higher_id) / 2)
3457 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3458 if not query.next():
3459 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3461 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3463 return str(higher_id)
3465 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3466 next_time = int(query.value(0))
3468 if target_time > next_time:
3472 if higher_id <= lower_id + 1:
3473 return str(higher_id)
3475 if target_time >= next_time:
3479 if higher_id <= lower_id + 1:
3480 return str(lower_id)
3482 def ConvertRelativeTime(self, val):
3487 elif suffix == "us":
3489 elif suffix == "ns":
3493 val = val[:-2].strip()
3494 if not self.IsNumber(val):
3496 val = int(val) * mult
3498 val += self.first_time
3500 val += self.last_time
3503 def ConvertTimeRange(self, vrange):
3505 vrange[0] = str(self.first_time)
3507 vrange[1] = str(self.last_time)
3508 vrange[0] = self.ConvertRelativeTime(vrange[0])
3509 vrange[1] = self.ConvertRelativeTime(vrange[1])
3510 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3512 beg_range = max(int(vrange[0]), self.first_time)
3513 end_range = min(int(vrange[1]), self.last_time)
3514 if beg_range > self.last_time or end_range < self.first_time:
3516 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3517 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3520 def AddTimeRange(self, value, ranges):
3521 n = value.count("-")
3525 if value.split("-")[1].strip() == "":
3531 pos = findnth(value, "-", n)
3532 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3533 if self.ConvertTimeRange(vrange):
3534 ranges.append(vrange)
3538 def DoValidate(self, input_string):
3540 for value in [x.strip() for x in input_string.split(",")]:
3541 if not self.AddTimeRange(value, ranges):
3542 return self.InvalidValue(value)
3543 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3544 self.value = " OR ".join(ranges)
3546 # Report Dialog Base
3548 class ReportDialogBase(QDialog):
3550 def __init__(self, glb, title, items, partial, parent=None):
3551 super(ReportDialogBase, self).__init__(parent)
3555 self.report_vars = ReportVars()
3557 self.setWindowTitle(title)
3558 self.setMinimumWidth(600)
3560 self.data_items = [x(glb, self) for x in items]
3562 self.partial = partial
3564 self.grid = QGridLayout()
3566 for row in xrange(len(self.data_items)):
3567 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3568 self.grid.addWidget(self.data_items[row].widget, row, 1)
3570 self.status = QLabel()
3572 self.ok_button = QPushButton("Ok", self)
3573 self.ok_button.setDefault(True)
3574 self.ok_button.released.connect(self.Ok)
3575 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3577 self.cancel_button = QPushButton("Cancel", self)
3578 self.cancel_button.released.connect(self.reject)
3579 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3581 self.hbox = QHBoxLayout()
3582 #self.hbox.addStretch()
3583 self.hbox.addWidget(self.status)
3584 self.hbox.addWidget(self.ok_button)
3585 self.hbox.addWidget(self.cancel_button)
3587 self.vbox = QVBoxLayout()
3588 self.vbox.addLayout(self.grid)
3589 self.vbox.addLayout(self.hbox)
3591 self.setLayout(self.vbox)
3594 vars = self.report_vars
3595 for d in self.data_items:
3596 if d.id == "REPORTNAME":
3599 self.ShowMessage("Report name is required")
3601 for d in self.data_items:
3604 for d in self.data_items[1:]:
3606 vars.limit = d.value
3608 if len(vars.where_clause):
3609 vars.where_clause += " AND "
3610 vars.where_clause += d.value
3611 if len(vars.where_clause):
3613 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3615 vars.where_clause = " WHERE " + vars.where_clause + " "
3618 def ShowMessage(self, msg):
3619 self.status.setText("<font color=#FF0000>" + msg)
3621 def ClearMessage(self):
3622 self.status.setText("")
3624 # Selected branch report creation dialog
3626 class SelectedBranchDialog(ReportDialogBase):
3628 def __init__(self, glb, parent=None):
3629 title = "Selected Branches"
3630 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3631 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3632 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3633 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3634 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3635 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3636 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3637 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3638 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3639 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3643 def GetEventList(db):
3645 query = QSqlQuery(db)
3646 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3648 events.append(query.value(0))
3651 # Is a table selectable
3653 def IsSelectable(db, table, sql = "", columns = "*"):
3654 query = QSqlQuery(db)
3656 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3661 # SQL table data model item
3663 class SQLTableItem():
3665 def __init__(self, row, data):
3669 def getData(self, column):
3670 return self.data[column]
3672 # SQL table data model
3674 class SQLTableModel(TableModel):
3676 progress = Signal(object)
3678 def __init__(self, glb, sql, column_headers, parent=None):
3679 super(SQLTableModel, self).__init__(parent)
3683 self.column_headers = column_headers
3684 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3685 self.fetcher.done.connect(self.Update)
3686 self.fetcher.Fetch(glb_chunk_sz)
3688 def DisplayData(self, item, index):
3689 self.FetchIfNeeded(item.row)
3690 return item.getData(index.column())
3692 def AddSample(self, data):
3693 child = SQLTableItem(self.populated, data)
3694 self.child_items.append(child)
3697 def Update(self, fetched):
3700 self.progress.emit(0)
3701 child_count = self.child_count
3702 count = self.populated - child_count
3704 parent = QModelIndex()
3705 self.beginInsertRows(parent, child_count, child_count + count - 1)
3706 self.insertRows(child_count, count, parent)
3707 self.child_count += count
3708 self.endInsertRows()
3709 self.progress.emit(self.child_count)
3711 def FetchMoreRecords(self, count):
3712 current = self.child_count
3714 self.fetcher.Fetch(count)
3716 self.progress.emit(0)
3719 def HasMoreRecords(self):
3722 def columnCount(self, parent=None):
3723 return len(self.column_headers)
3725 def columnHeader(self, column):
3726 return self.column_headers[column]
3728 def SQLTableDataPrep(self, query, count):
3730 for i in xrange(count):
3731 data.append(query.value(i))
3734 # SQL automatic table data model
3736 class SQLAutoTableModel(SQLTableModel):
3738 def __init__(self, glb, table_name, parent=None):
3739 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3740 if table_name == "comm_threads_view":
3741 # For now, comm_threads_view has no id column
3742 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3744 query = QSqlQuery(glb.db)
3745 if glb.dbref.is_sqlite3:
3746 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3748 column_headers.append(query.value(1))
3749 if table_name == "sqlite_master":
3750 sql = "SELECT * FROM " + table_name
3752 if table_name[:19] == "information_schema.":
3753 sql = "SELECT * FROM " + table_name
3754 select_table_name = table_name[19:]
3755 schema = "information_schema"
3757 select_table_name = table_name
3759 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3761 column_headers.append(query.value(0))
3762 if pyside_version_1 and sys.version_info[0] == 3:
3763 if table_name == "samples_view":
3764 self.SQLTableDataPrep = self.samples_view_DataPrep
3765 if table_name == "samples":
3766 self.SQLTableDataPrep = self.samples_DataPrep
3767 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3769 def samples_view_DataPrep(self, query, count):
3771 data.append(query.value(0))
3772 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3773 data.append("{:>19}".format(query.value(1)))
3774 for i in xrange(2, count):
3775 data.append(query.value(i))
3778 def samples_DataPrep(self, query, count):
3781 data.append(query.value(i))
3782 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3783 data.append("{:>19}".format(query.value(9)))
3784 for i in xrange(10, count):
3785 data.append(query.value(i))
3788 # Base class for custom ResizeColumnsToContents
3790 class ResizeColumnsToContentsBase(QObject):
3792 def __init__(self, parent=None):
3793 super(ResizeColumnsToContentsBase, self).__init__(parent)
3795 def ResizeColumnToContents(self, column, n):
3796 # Using the view's resizeColumnToContents() here is extrememly slow
3797 # so implement a crude alternative
3798 font = self.view.font()
3799 metrics = QFontMetrics(font)
3801 for row in xrange(n):
3802 val = self.data_model.child_items[row].data[column]
3803 len = metrics.width(str(val) + "MM")
3804 max = len if len > max else max
3805 val = self.data_model.columnHeader(column)
3806 len = metrics.width(str(val) + "MM")
3807 max = len if len > max else max
3808 self.view.setColumnWidth(column, max)
3810 def ResizeColumnsToContents(self):
3811 n = min(self.data_model.child_count, 100)
3813 # No data yet, so connect a signal to notify when there is
3814 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3816 columns = self.data_model.columnCount()
3817 for i in xrange(columns):
3818 self.ResizeColumnToContents(i, n)
3820 def UpdateColumnWidths(self, *x):
3821 # This only needs to be done once, so disconnect the signal now
3822 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3823 self.ResizeColumnsToContents()
3825 # Convert value to CSV
3829 val = val.replace('"', '""')
3830 if "," in val or '"' in val:
3831 val = '"' + val + '"'
3834 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3838 def RowColumnKey(a):
3839 return a.row() * glb_max_cols + a.column()
3841 # Copy selected table cells to clipboard
3843 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3844 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3845 idx_cnt = len(indexes)
3850 min_row = indexes[0].row()
3851 max_row = indexes[0].row()
3852 min_col = indexes[0].column()
3853 max_col = indexes[0].column()
3855 min_row = min(min_row, i.row())
3856 max_row = max(max_row, i.row())
3857 min_col = min(min_col, i.column())
3858 max_col = max(max_col, i.column())
3859 if max_col > glb_max_cols:
3860 raise RuntimeError("glb_max_cols is too low")
3861 max_width = [0] * (1 + max_col - min_col)
3863 c = i.column() - min_col
3864 max_width[c] = max(max_width[c], len(str(i.data())))
3869 model = indexes[0].model()
3870 for col in range(min_col, max_col + 1):
3871 val = model.headerData(col, Qt.Horizontal)
3873 text += sep + ToCSValue(val)
3877 max_width[c] = max(max_width[c], len(val))
3878 width = max_width[c]
3879 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3880 if align & Qt.AlignRight:
3881 val = val.rjust(width)
3882 text += pad + sep + val
3883 pad = " " * (width - len(val))
3890 if i.row() > last_row:
3896 text += sep + ToCSValue(str(i.data()))
3899 width = max_width[i.column() - min_col]
3900 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3901 val = str(i.data()).rjust(width)
3904 text += pad + sep + val
3905 pad = " " * (width - len(val))
3907 QApplication.clipboard().setText(text)
3909 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3910 indexes = view.selectedIndexes()
3911 if not len(indexes):
3914 selection = view.selectionModel()
3918 above = view.indexAbove(i)
3919 if not selection.isSelected(above):
3924 raise RuntimeError("CopyTreeCellsToClipboard internal error")
3926 model = first.model()
3928 col_cnt = model.columnCount(first)
3929 max_width = [0] * col_cnt
3932 indent_str = " " * indent_sz
3934 expanded_mark_sz = 2
3935 if sys.version_info[0] == 3:
3936 expanded_mark = "\u25BC "
3937 not_expanded_mark = "\u25B6 "
3939 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3940 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3948 for c in range(col_cnt):
3949 i = pos.sibling(row, c)
3951 n = len(str(i.data()))
3953 n = len(str(i.data()).strip())
3954 n += (i.internalPointer().level - 1) * indent_sz
3955 n += expanded_mark_sz
3956 max_width[c] = max(max_width[c], n)
3957 pos = view.indexBelow(pos)
3958 if not selection.isSelected(pos):
3965 for c in range(col_cnt):
3966 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3968 text += sep + ToCSValue(val)
3971 max_width[c] = max(max_width[c], len(val))
3972 width = max_width[c]
3973 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3974 if align & Qt.AlignRight:
3975 val = val.rjust(width)
3976 text += pad + sep + val
3977 pad = " " * (width - len(val))
3986 for c in range(col_cnt):
3987 i = pos.sibling(row, c)
3990 if model.hasChildren(i):
3991 if view.isExpanded(i):
3992 mark = expanded_mark
3994 mark = not_expanded_mark
3997 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
3999 text += sep + ToCSValue(val)
4002 width = max_width[c]
4003 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
4004 val = val.rjust(width)
4005 text += pad + sep + val
4006 pad = " " * (width - len(val))
4008 pos = view.indexBelow(pos)
4009 if not selection.isSelected(pos):
4011 text = text.rstrip() + "\n"
4015 QApplication.clipboard().setText(text)
4017 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4018 view.CopyCellsToClipboard(view, as_csv, with_hdr)
4020 def CopyCellsToClipboardHdr(view):
4021 CopyCellsToClipboard(view, False, True)
4023 def CopyCellsToClipboardCSV(view):
4024 CopyCellsToClipboard(view, True, True)
4028 class ContextMenu(object):
4030 def __init__(self, view):
4032 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4033 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4035 def ShowContextMenu(self, pos):
4036 menu = QMenu(self.view)
4037 self.AddActions(menu)
4038 menu.exec_(self.view.mapToGlobal(pos))
4040 def AddCopy(self, menu):
4041 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4042 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4044 def AddActions(self, menu):
4047 class TreeContextMenu(ContextMenu):
4049 def __init__(self, view):
4050 super(TreeContextMenu, self).__init__(view)
4052 def AddActions(self, menu):
4053 i = self.view.currentIndex()
4054 text = str(i.data()).strip()
4056 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4061 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4063 def __init__(self, glb, table_name, parent=None):
4064 super(TableWindow, self).__init__(parent)
4066 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4068 self.model = QSortFilterProxyModel()
4069 self.model.setSourceModel(self.data_model)
4071 self.view = QTableView()
4072 self.view.setModel(self.model)
4073 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4074 self.view.verticalHeader().setVisible(False)
4075 self.view.sortByColumn(-1, Qt.AscendingOrder)
4076 self.view.setSortingEnabled(True)
4077 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4078 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4080 self.ResizeColumnsToContents()
4082 self.context_menu = ContextMenu(self.view)
4084 self.find_bar = FindBar(self, self, True)
4086 self.finder = ChildDataItemFinder(self.data_model)
4088 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4090 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4092 self.setWidget(self.vbox.Widget())
4094 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4096 def Find(self, value, direction, pattern, context):
4097 self.view.setFocus()
4098 self.find_bar.Busy()
4099 self.finder.Find(value, direction, pattern, context, self.FindDone)
4101 def FindDone(self, row):
4102 self.find_bar.Idle()
4104 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4106 self.find_bar.NotFound()
4110 def GetTableList(glb):
4112 query = QSqlQuery(glb.db)
4113 if glb.dbref.is_sqlite3:
4114 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4116 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4118 tables.append(query.value(0))
4119 if glb.dbref.is_sqlite3:
4120 tables.append("sqlite_master")
4122 tables.append("information_schema.tables")
4123 tables.append("information_schema.views")
4124 tables.append("information_schema.columns")
4127 # Top Calls data model
4129 class TopCallsModel(SQLTableModel):
4131 def __init__(self, glb, report_vars, parent=None):
4133 if not glb.dbref.is_sqlite3:
4136 if len(report_vars.limit):
4137 limit = " LIMIT " + report_vars.limit
4138 sql = ("SELECT comm, pid, tid, name,"
4140 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4143 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4145 " WHEN (calls.flags = 1) THEN 'no call'" + text +
4146 " WHEN (calls.flags = 2) THEN 'no return'" + text +
4147 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4151 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4152 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4153 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4154 " INNER JOIN comms ON calls.comm_id = comms.id"
4155 " INNER JOIN threads ON calls.thread_id = threads.id" +
4156 report_vars.where_clause +
4157 " ORDER BY elapsed_time DESC" +
4160 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4161 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4162 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4164 def columnAlignment(self, column):
4165 return self.alignment[column]
4167 # Top Calls report creation dialog
4169 class TopCallsDialog(ReportDialogBase):
4171 def __init__(self, glb, parent=None):
4172 title = "Top Calls by Elapsed Time"
4173 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4174 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4175 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4176 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4177 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4178 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4179 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4180 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4181 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4185 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4187 def __init__(self, glb, report_vars, parent=None):
4188 super(TopCallsWindow, self).__init__(parent)
4190 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4191 self.model = self.data_model
4193 self.view = QTableView()
4194 self.view.setModel(self.model)
4195 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4196 self.view.verticalHeader().setVisible(False)
4197 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4198 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4200 self.context_menu = ContextMenu(self.view)
4202 self.ResizeColumnsToContents()
4204 self.find_bar = FindBar(self, self, True)
4206 self.finder = ChildDataItemFinder(self.model)
4208 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4210 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4212 self.setWidget(self.vbox.Widget())
4214 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4216 def Find(self, value, direction, pattern, context):
4217 self.view.setFocus()
4218 self.find_bar.Busy()
4219 self.finder.Find(value, direction, pattern, context, self.FindDone)
4221 def FindDone(self, row):
4222 self.find_bar.Idle()
4224 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4226 self.find_bar.NotFound()
4230 def CreateAction(label, tip, callback, parent=None, shortcut=None):
4231 action = QAction(label, parent)
4232 if shortcut != None:
4233 action.setShortcuts(shortcut)
4234 action.setStatusTip(tip)
4235 action.triggered.connect(callback)
4238 # Typical application actions
4240 def CreateExitAction(app, parent=None):
4241 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4243 # Typical MDI actions
4245 def CreateCloseActiveWindowAction(mdi_area):
4246 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4248 def CreateCloseAllWindowsAction(mdi_area):
4249 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4251 def CreateTileWindowsAction(mdi_area):
4252 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4254 def CreateCascadeWindowsAction(mdi_area):
4255 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4257 def CreateNextWindowAction(mdi_area):
4258 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4260 def CreatePreviousWindowAction(mdi_area):
4261 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4263 # Typical MDI window menu
4267 def __init__(self, mdi_area, menu):
4268 self.mdi_area = mdi_area
4269 self.window_menu = menu.addMenu("&Windows")
4270 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4271 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4272 self.tile_windows = CreateTileWindowsAction(mdi_area)
4273 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4274 self.next_window = CreateNextWindowAction(mdi_area)
4275 self.previous_window = CreatePreviousWindowAction(mdi_area)
4276 self.window_menu.aboutToShow.connect(self.Update)
4279 self.window_menu.clear()
4280 sub_window_count = len(self.mdi_area.subWindowList())
4281 have_sub_windows = sub_window_count != 0
4282 self.close_active_window.setEnabled(have_sub_windows)
4283 self.close_all_windows.setEnabled(have_sub_windows)
4284 self.tile_windows.setEnabled(have_sub_windows)
4285 self.cascade_windows.setEnabled(have_sub_windows)
4286 self.next_window.setEnabled(have_sub_windows)
4287 self.previous_window.setEnabled(have_sub_windows)
4288 self.window_menu.addAction(self.close_active_window)
4289 self.window_menu.addAction(self.close_all_windows)
4290 self.window_menu.addSeparator()
4291 self.window_menu.addAction(self.tile_windows)
4292 self.window_menu.addAction(self.cascade_windows)
4293 self.window_menu.addSeparator()
4294 self.window_menu.addAction(self.next_window)
4295 self.window_menu.addAction(self.previous_window)
4296 if sub_window_count == 0:
4298 self.window_menu.addSeparator()
4300 for sub_window in self.mdi_area.subWindowList():
4301 label = str(nr) + " " + sub_window.name
4304 action = self.window_menu.addAction(label)
4305 action.setCheckable(True)
4306 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4307 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4308 self.window_menu.addAction(action)
4311 def setActiveSubWindow(self, nr):
4312 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4327 <p class=c1><a href=#reports>1. Reports</a></p>
4328 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4329 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4330 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
4331 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4332 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4333 <p class=c1><a href=#charts>2. Charts</a></p>
4334 <p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4335 <p class=c1><a href=#tables>3. Tables</a></p>
4336 <h1 id=reports>1. Reports</h1>
4337 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4338 The result is a GUI window with a tree representing a context-sensitive
4339 call-graph. Expanding a couple of levels of the tree and adjusting column
4340 widths to suit will display something like:
4342 Call Graph: pt_example
4343 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
4346 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
4347 |- unknown unknown 1 13198 0.1 1 0.0
4348 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
4349 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
4350 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
4351 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
4352 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
4353 >- __libc_csu_init ls 1 10354 0.1 10 0.0
4354 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
4355 v- main ls 1 8182043 99.6 180254 99.9
4357 <h3>Points to note:</h3>
4359 <li>The top level is a command name (comm)</li>
4360 <li>The next level is a thread (pid:tid)</li>
4361 <li>Subsequent levels are functions</li>
4362 <li>'Count' is the number of calls</li>
4363 <li>'Time' is the elapsed time until the function returns</li>
4364 <li>Percentages are relative to the level above</li>
4365 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
4368 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4369 The pattern matching symbols are ? for any character and * for zero or more characters.
4370 <h2 id=calltree>1.2 Call Tree</h2>
4371 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4372 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4373 <h2 id=allbranches>1.3 All branches</h2>
4374 The All branches report displays all branches in chronological order.
4375 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4376 <h3>Disassembly</h3>
4377 Open a branch to display disassembly. This only works if:
4379 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4380 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4381 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4382 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4383 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4385 <h4 id=xed>Intel XED Setup</h4>
4386 To use Intel XED, libxed.so must be present. To build and install libxed.so:
4388 git clone https://github.com/intelxed/mbuild.git mbuild
4389 git clone https://github.com/intelxed/xed
4392 sudo ./mfile.py --prefix=/usr/local install
4395 <h3>Instructions per Cycle (IPC)</h3>
4396 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4397 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4398 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4399 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4400 since the previous displayed 'IPC'.
4402 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4403 Refer to Python documentation for the regular expression syntax.
4404 All columns are searched, but only currently fetched rows are searched.
4405 <h2 id=selectedbranches>1.4 Selected branches</h2>
4406 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4407 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4408 <h3>1.4.1 Time ranges</h3>
4409 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4410 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
4412 81073085947329-81073085958238 From 81073085947329 to 81073085958238
4413 100us-200us From 100us to 200us
4414 10ms- From 10ms to the end
4415 -100ns The first 100ns
4416 -10ms- The last 10ms
4418 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4419 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4420 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.
4421 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4422 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4423 <h1 id=charts>2. Charts</h1>
4424 <h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4425 This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4428 <li>Mouse over to highight the task and show the time</li>
4429 <li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4430 <li>Go back and forward by pressing the arrow buttons</li>
4431 <li>If call information is available, right-click to show a call tree opened to that task and time.
4432 Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4436 The graph can be misleading in the following respects:
4438 <li>The graph shows the first task on each CPU as running from the beginning of the time range.
4439 Because tracing might start on different CPUs at different times, that is not necessarily the case.
4440 Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4441 <li>Similarly, the last task on each CPU can be showing running longer than it really was.
4442 Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4443 <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>
4445 <h1 id=tables>3. Tables</h1>
4446 The Tables menu shows all tables and views in the database. Most tables have an associated view
4447 which displays the information in a more friendly way. Not all data for large tables is fetched
4448 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4449 but that can be slow for large tables.
4450 <p>There are also tables of database meta-information.
4451 For SQLite3 databases, the sqlite_master table is included.
4452 For PostgreSQL databases, information_schema.tables/views/columns are included.
4454 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4455 Refer to Python documentation for the regular expression syntax.
4456 All columns are searched, but only currently fetched rows are searched.
4457 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4458 will go to the next/previous result in id order, instead of display order.
4463 class HelpWindow(QMdiSubWindow):
4465 def __init__(self, glb, parent=None):
4466 super(HelpWindow, self).__init__(parent)
4468 self.text = QTextBrowser()
4469 self.text.setHtml(glb_help_text)
4470 self.text.setReadOnly(True)
4471 self.text.setOpenExternalLinks(True)
4473 self.setWidget(self.text)
4475 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4477 # Main window that only displays the help text
4479 class HelpOnlyWindow(QMainWindow):
4481 def __init__(self, parent=None):
4482 super(HelpOnlyWindow, self).__init__(parent)
4484 self.setMinimumSize(200, 100)
4485 self.resize(800, 600)
4486 self.setWindowTitle("Exported SQL Viewer Help")
4487 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4489 self.text = QTextBrowser()
4490 self.text.setHtml(glb_help_text)
4491 self.text.setReadOnly(True)
4492 self.text.setOpenExternalLinks(True)
4494 self.setCentralWidget(self.text)
4496 # PostqreSQL server version
4498 def PostqreSQLServerVersion(db):
4499 query = QSqlQuery(db)
4500 QueryExec(query, "SELECT VERSION()")
4502 v_str = query.value(0)
4503 v_list = v_str.strip().split(" ")
4504 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4511 def SQLiteVersion(db):
4512 query = QSqlQuery(db)
4513 QueryExec(query, "SELECT sqlite_version()")
4515 return query.value(0)
4520 class AboutDialog(QDialog):
4522 def __init__(self, glb, parent=None):
4523 super(AboutDialog, self).__init__(parent)
4525 self.setWindowTitle("About Exported SQL Viewer")
4526 self.setMinimumWidth(300)
4528 pyside_version = "1" if pyside_version_1 else "2"
4531 text += "Python version: " + sys.version.split(" ")[0] + "\n"
4532 text += "PySide version: " + pyside_version + "\n"
4533 text += "Qt version: " + qVersion() + "\n"
4534 if glb.dbref.is_sqlite3:
4535 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
4537 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4540 self.text = QTextBrowser()
4541 self.text.setHtml(text)
4542 self.text.setReadOnly(True)
4543 self.text.setOpenExternalLinks(True)
4545 self.vbox = QVBoxLayout()
4546 self.vbox.addWidget(self.text)
4548 self.setLayout(self.vbox)
4552 def ResizeFont(widget, diff):
4553 font = widget.font()
4554 sz = font.pointSize()
4555 font.setPointSize(sz + diff)
4556 widget.setFont(font)
4558 def ShrinkFont(widget):
4559 ResizeFont(widget, -1)
4561 def EnlargeFont(widget):
4562 ResizeFont(widget, 1)
4564 # Unique name for sub-windows
4566 def NumberedWindowName(name, nr):
4568 name += " <" + str(nr) + ">"
4571 def UniqueSubWindowName(mdi_area, name):
4574 unique_name = NumberedWindowName(name, nr)
4576 for sub_window in mdi_area.subWindowList():
4577 if sub_window.name == unique_name:
4586 def AddSubWindow(mdi_area, sub_window, name):
4587 unique_name = UniqueSubWindowName(mdi_area, name)
4588 sub_window.setMinimumSize(200, 100)
4589 sub_window.resize(800, 600)
4590 sub_window.setWindowTitle(unique_name)
4591 sub_window.setAttribute(Qt.WA_DeleteOnClose)
4592 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4593 sub_window.name = unique_name
4594 mdi_area.addSubWindow(sub_window)
4599 class MainWindow(QMainWindow):
4601 def __init__(self, glb, parent=None):
4602 super(MainWindow, self).__init__(parent)
4606 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4607 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4608 self.setMinimumSize(200, 100)
4610 self.mdi_area = QMdiArea()
4611 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4612 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4614 self.setCentralWidget(self.mdi_area)
4616 menu = self.menuBar()
4618 file_menu = menu.addMenu("&File")
4619 file_menu.addAction(CreateExitAction(glb.app, self))
4621 edit_menu = menu.addMenu("&Edit")
4622 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4623 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4624 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4625 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4626 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4627 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4629 reports_menu = menu.addMenu("&Reports")
4630 if IsSelectable(glb.db, "calls"):
4631 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4633 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4634 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4636 self.EventMenu(GetEventList(glb.db), reports_menu)
4638 if IsSelectable(glb.db, "calls"):
4639 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4641 if IsSelectable(glb.db, "context_switches"):
4642 charts_menu = menu.addMenu("&Charts")
4643 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4645 self.TableMenu(GetTableList(glb), menu)
4647 self.window_menu = WindowMenu(self.mdi_area, menu)
4649 help_menu = menu.addMenu("&Help")
4650 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4651 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4654 win = self.mdi_area.activeSubWindow()
4661 def CopyToClipboard(self):
4662 self.Try(CopyCellsToClipboardHdr)
4664 def CopyToClipboardCSV(self):
4665 self.Try(CopyCellsToClipboardCSV)
4668 win = self.mdi_area.activeSubWindow()
4671 win.find_bar.Activate()
4675 def FetchMoreRecords(self):
4676 win = self.mdi_area.activeSubWindow()
4679 win.fetch_bar.Activate()
4683 def ShrinkFont(self):
4684 self.Try(ShrinkFont)
4686 def EnlargeFont(self):
4687 self.Try(EnlargeFont)
4689 def EventMenu(self, events, reports_menu):
4691 for event in events:
4692 event = event.split(":")[0]
4693 if event == "branches":
4694 branches_events += 1
4696 for event in events:
4698 event = event.split(":")[0]
4699 if event == "branches":
4700 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4701 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4702 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4703 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4705 def TimeChartByCPU(self):
4706 TimeChartByCPUWindow(self.glb, self)
4708 def TableMenu(self, tables, menu):
4709 table_menu = menu.addMenu("&Tables")
4710 for table in tables:
4711 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4713 def NewCallGraph(self):
4714 CallGraphWindow(self.glb, self)
4716 def NewCallTree(self):
4717 CallTreeWindow(self.glb, self)
4719 def NewTopCalls(self):
4720 dialog = TopCallsDialog(self.glb, self)
4721 ret = dialog.exec_()
4723 TopCallsWindow(self.glb, dialog.report_vars, self)
4725 def NewBranchView(self, event_id):
4726 BranchWindow(self.glb, event_id, ReportVars(), self)
4728 def NewSelectedBranchView(self, event_id):
4729 dialog = SelectedBranchDialog(self.glb, self)
4730 ret = dialog.exec_()
4732 BranchWindow(self.glb, event_id, dialog.report_vars, self)
4734 def NewTableView(self, table_name):
4735 TableWindow(self.glb, table_name, self)
4738 HelpWindow(self.glb, self)
4741 dialog = AboutDialog(self.glb, self)
4746 class xed_state_t(Structure):
4753 class XEDInstruction():
4755 def __init__(self, libxed):
4756 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4757 xedd_t = c_byte * 512
4758 self.xedd = xedd_t()
4759 self.xedp = addressof(self.xedd)
4760 libxed.xed_decoded_inst_zero(self.xedp)
4761 self.state = xed_state_t()
4762 self.statep = addressof(self.state)
4763 # Buffer for disassembled instruction text
4764 self.buffer = create_string_buffer(256)
4765 self.bufferp = addressof(self.buffer)
4771 self.libxed = CDLL("libxed.so")
4775 self.libxed = CDLL("/usr/local/lib/libxed.so")
4777 self.xed_tables_init = self.libxed.xed_tables_init
4778 self.xed_tables_init.restype = None
4779 self.xed_tables_init.argtypes = []
4781 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4782 self.xed_decoded_inst_zero.restype = None
4783 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4785 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4786 self.xed_operand_values_set_mode.restype = None
4787 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4789 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4790 self.xed_decoded_inst_zero_keep_mode.restype = None
4791 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4793 self.xed_decode = self.libxed.xed_decode
4794 self.xed_decode.restype = c_int
4795 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4797 self.xed_format_context = self.libxed.xed_format_context
4798 self.xed_format_context.restype = c_uint
4799 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4801 self.xed_tables_init()
4803 def Instruction(self):
4804 return XEDInstruction(self)
4806 def SetMode(self, inst, mode):
4808 inst.state.mode = 4 # 32-bit
4809 inst.state.width = 4 # 4 bytes
4811 inst.state.mode = 1 # 64-bit
4812 inst.state.width = 8 # 8 bytes
4813 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4815 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4816 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4817 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4820 # Use AT&T mode (2), alternative is Intel (3)
4821 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4824 if sys.version_info[0] == 2:
4825 result = inst.buffer.value
4827 result = inst.buffer.value.decode()
4828 # Return instruction length and the disassembled instruction text
4829 # For now, assume the length is in byte 166
4830 return inst.xedd[166], result
4832 def TryOpen(file_name):
4834 return open(file_name, "rb")
4839 result = sizeof(c_void_p)
4846 if sys.version_info[0] == 2:
4847 eclass = ord(header[4])
4848 encoding = ord(header[5])
4849 version = ord(header[6])
4852 encoding = header[5]
4854 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4855 result = True if eclass == 2 else False
4862 def __init__(self, dbref, db, dbname):
4865 self.dbname = dbname
4866 self.home_dir = os.path.expanduser("~")
4867 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4868 if self.buildid_dir:
4869 self.buildid_dir += "/.build-id/"
4871 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4873 self.mainwindow = None
4874 self.instances_to_shutdown_on_exit = weakref.WeakSet()
4876 self.disassembler = LibXED()
4877 self.have_disassembler = True
4879 self.have_disassembler = False
4880 self.host_machine_id = 0
4881 self.host_start_time = 0
4882 self.host_finish_time = 0
4884 def FileFromBuildId(self, build_id):
4885 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4886 return TryOpen(file_name)
4888 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4889 # Assume current machine i.e. no support for virtualization
4890 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4891 file_name = os.getenv("PERF_KCORE")
4892 f = TryOpen(file_name) if file_name else None
4895 # For now, no special handling if long_name is /proc/kcore
4896 f = TryOpen(long_name)
4899 f = self.FileFromBuildId(build_id)
4904 def AddInstanceToShutdownOnExit(self, instance):
4905 self.instances_to_shutdown_on_exit.add(instance)
4907 # Shutdown any background processes or threads
4908 def ShutdownInstances(self):
4909 for x in self.instances_to_shutdown_on_exit:
4915 def GetHostMachineId(self):
4916 query = QSqlQuery(self.db)
4917 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4919 self.host_machine_id = query.value(0)
4921 self.host_machine_id = 0
4922 return self.host_machine_id
4924 def HostMachineId(self):
4925 if self.host_machine_id:
4926 return self.host_machine_id
4927 return self.GetHostMachineId()
4929 def SelectValue(self, sql):
4930 query = QSqlQuery(self.db)
4932 QueryExec(query, sql)
4936 return Decimal(query.value(0))
4939 def SwitchesMinTime(self, machine_id):
4940 return self.SelectValue("SELECT time"
4941 " FROM context_switches"
4942 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4943 " ORDER BY id LIMIT 1")
4945 def SwitchesMaxTime(self, machine_id):
4946 return self.SelectValue("SELECT time"
4947 " FROM context_switches"
4948 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4949 " ORDER BY id DESC LIMIT 1")
4951 def SamplesMinTime(self, machine_id):
4952 return self.SelectValue("SELECT time"
4954 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4955 " ORDER BY id LIMIT 1")
4957 def SamplesMaxTime(self, machine_id):
4958 return self.SelectValue("SELECT time"
4960 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4961 " ORDER BY id DESC LIMIT 1")
4963 def CallsMinTime(self, machine_id):
4964 return self.SelectValue("SELECT calls.call_time"
4966 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4967 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4968 " ORDER BY calls.id LIMIT 1")
4970 def CallsMaxTime(self, machine_id):
4971 return self.SelectValue("SELECT calls.return_time"
4973 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4974 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4975 " ORDER BY calls.return_time DESC LIMIT 1")
4977 def GetStartTime(self, machine_id):
4978 t0 = self.SwitchesMinTime(machine_id)
4979 t1 = self.SamplesMinTime(machine_id)
4980 t2 = self.CallsMinTime(machine_id)
4981 if t0 is None or (not(t1 is None) and t1 < t0):
4983 if t0 is None or (not(t2 is None) and t2 < t0):
4987 def GetFinishTime(self, machine_id):
4988 t0 = self.SwitchesMaxTime(machine_id)
4989 t1 = self.SamplesMaxTime(machine_id)
4990 t2 = self.CallsMaxTime(machine_id)
4991 if t0 is None or (not(t1 is None) and t1 > t0):
4993 if t0 is None or (not(t2 is None) and t2 > t0):
4997 def HostStartTime(self):
4998 if self.host_start_time:
4999 return self.host_start_time
5000 self.host_start_time = self.GetStartTime(self.HostMachineId())
5001 return self.host_start_time
5003 def HostFinishTime(self):
5004 if self.host_finish_time:
5005 return self.host_finish_time
5006 self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5007 return self.host_finish_time
5009 def StartTime(self, machine_id):
5010 if machine_id == self.HostMachineId():
5011 return self.HostStartTime()
5012 return self.GetStartTime(machine_id)
5014 def FinishTime(self, machine_id):
5015 if machine_id == self.HostMachineId():
5016 return self.HostFinishTime()
5017 return self.GetFinishTime(machine_id)
5019 # Database reference
5023 def __init__(self, is_sqlite3, dbname):
5024 self.is_sqlite3 = is_sqlite3
5025 self.dbname = dbname
5027 self.FALSE = "FALSE"
5028 # SQLite prior to version 3.23 does not support TRUE and FALSE
5033 def Open(self, connection_name):
5034 dbname = self.dbname
5036 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5038 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5039 opts = dbname.split()
5042 opt = opt.split("=")
5043 if opt[0] == "hostname":
5044 db.setHostName(opt[1])
5045 elif opt[0] == "port":
5046 db.setPort(int(opt[1]))
5047 elif opt[0] == "username":
5048 db.setUserName(opt[1])
5049 elif opt[0] == "password":
5050 db.setPassword(opt[1])
5051 elif opt[0] == "dbname":
5056 db.setDatabaseName(dbname)
5058 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5064 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5065 " or: exported-sql-viewer.py --help-only"
5066 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5067 ap.add_argument("--pyside-version-1", action='store_true')
5068 ap.add_argument("dbname", nargs="?")
5069 ap.add_argument("--help-only", action='store_true')
5070 args = ap.parse_args()
5073 app = QApplication(sys.argv)
5074 mainwindow = HelpOnlyWindow()
5079 dbname = args.dbname
5082 print("Too few arguments")
5087 f = open(dbname, "rb")
5088 if f.read(15) == b'SQLite format 3':
5094 dbref = DBRef(is_sqlite3, dbname)
5095 db, dbname = dbref.Open("main")
5096 glb = Glb(dbref, db, dbname)
5097 app = QApplication(sys.argv)
5099 mainwindow = MainWindow(glb)
5100 glb.mainwindow = mainwindow
5103 glb.ShutdownInstances()
5107 if __name__ == "__main__":