treewide: remove redundant IS_ERR() before error code check
[linux/fpc-iii.git] / tools / perf / scripts / python / exported-sql-viewer.py
blob26d7be785288293abf23f33f86776302c56dc0f7
1 #!/usr/bin/env python
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
8 # scripts for details.
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(%)
26 # v- ls
27 # v- 2638:2638
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
39 # Points to note:
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
53 # libxed.so:
54 # git clone https://github.com/intelxed/mbuild.git mbuild
55 # git clone https://github.com/intelxed/xed
56 # cd xed
57 # ./mfile.py --share
58 # sudo ./mfile.py --prefix=/usr/local install
59 # sudo ldconfig
61 # Example report:
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
93 import sys
94 import argparse
95 import weakref
96 import threading
97 import string
98 try:
99 # Python2
100 import cPickle as pickle
101 # size of pickled integer big enough for record size
102 glb_nsz = 8
103 except ImportError:
104 import pickle
105 glb_nsz = 16
106 import re
107 import os
108 import random
109 import copy
110 import math
112 pyside_version_1 = True
113 if not "--pyside-version-1" in sys.argv:
114 try:
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
120 except:
121 pass
123 if pyside_version_1:
124 from PySide.QtCore import *
125 from PySide.QtGui import *
126 from PySide.QtSql import *
128 from decimal import *
129 from ctypes import *
130 from multiprocessing import Process, Array, Value, Event
132 # xrange is range in Python3
133 try:
134 xrange
135 except NameError:
136 xrange = range
138 def printerr(*args, **keyword_args):
139 print(*args, file=sys.stderr, **keyword_args)
141 # Data formatting helpers
143 def tohex(ip):
144 if ip < 0:
145 ip += 1 << 64
146 return "%x" % ip
148 def offstr(offset):
149 if offset:
150 return "+0x%x" % offset
151 return ""
153 def dsoname(name):
154 if name == "[kernel.kallsyms]":
155 return "[kernel]"
156 return name
158 def findnth(s, sub, n, offs=0):
159 pos = s.find(sub)
160 if pos < 0:
161 return pos
162 if n <= 1:
163 return offs + pos
164 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
166 # Percent to one decimal place
168 def PercentToOneDP(n, d):
169 if not d:
170 return "0.0"
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)
178 if not ret:
179 raise Exception("Query failed: " + query.lastError().text())
181 # Background thread
183 class Thread(QThread):
185 done = Signal(object)
187 def __init__(self, task, param=None, parent=None):
188 super(Thread, self).__init__(parent)
189 self.task = task
190 self.param = param
192 def run(self):
193 while True:
194 if self.param is None:
195 done, result = self.task()
196 else:
197 done, result = self.task(self.param)
198 self.done.emit(result)
199 if done:
200 break
202 # Tree data model
204 class TreeModel(QAbstractItemModel):
206 def __init__(self, glb, params, parent=None):
207 super(TreeModel, self).__init__(parent)
208 self.glb = glb
209 self.params = params
210 self.root = self.GetRoot()
211 self.last_row_read = 0
213 def Item(self, parent):
214 if parent.isValid():
215 return parent.internalPointer()
216 else:
217 return self.root
219 def rowCount(self, parent):
220 result = self.Item(parent).childCount()
221 if result < 0:
222 result = 0
223 self.dataChanged.emit(parent, parent)
224 return result
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:
233 return None
234 if orientation != Qt.Horizontal:
235 return None
236 return self.columnHeader(section)
238 def parent(self, child):
239 child_item = child.internalPointer()
240 if child_item is self.root:
241 return QModelIndex()
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):
259 return Qt.AlignLeft
261 def columnFont(self, column):
262 return None
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:
270 return None
271 item = index.internalPointer()
272 return self.DisplayData(item, index)
274 # Table data model
276 class TableModel(QAbstractTableModel):
278 def __init__(self, parent=None):
279 super(TableModel, self).__init__(parent)
280 self.child_count = 0
281 self.child_items = []
282 self.last_row_read = 0
284 def Item(self, parent):
285 if parent.isValid():
286 return parent.internalPointer()
287 else:
288 return self
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:
297 return None
298 if orientation != Qt.Horizontal:
299 return None
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):
315 return Qt.AlignLeft
317 def columnFont(self, column):
318 return None
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:
326 return None
327 item = index.internalPointer()
328 return self.DisplayData(item, index)
330 # Model cache
332 model_cache = weakref.WeakValueDictionary()
333 model_cache_lock = threading.Lock()
335 def LookupCreateModel(model_name, create_fn):
336 model_cache_lock.acquire()
337 try:
338 model = model_cache[model_name]
339 except:
340 model = None
341 if model is None:
342 model = create_fn()
343 model_cache[model_name] = model
344 model_cache_lock.release()
345 return model
347 def LookupModel(model_name):
348 model_cache_lock.acquire()
349 try:
350 model = model_cache[model_name]
351 except:
352 model = None
353 model_cache_lock.release()
354 return model
356 # Find bar
358 class FindBar():
360 def __init__(self, parent, finder, is_reg_expr=False):
361 self.finder = finder
362 self.context = []
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)
375 self.progress.hide()
377 if is_reg_expr:
378 self.pattern = QCheckBox("Regular Expression")
379 else:
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)
406 self.bar = QWidget()
407 self.bar.setLayout(self.hbox)
408 self.bar.hide()
410 def Widget(self):
411 return self.bar
413 def Activate(self):
414 self.bar.show()
415 self.textbox.lineEdit().selectAll()
416 self.textbox.setFocus()
418 def Deactivate(self):
419 self.bar.hide()
421 def Busy(self):
422 self.textbox.setEnabled(False)
423 self.pattern.hide()
424 self.next_button.hide()
425 self.prev_button.hide()
426 self.progress.show()
428 def Idle(self):
429 self.textbox.setEnabled(True)
430 self.progress.hide()
431 self.pattern.show()
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
448 if data == None:
449 self.textbox.setItemData(index, pattern)
450 else:
451 self.pattern.setChecked(data)
452 self.Find(0)
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
460 if index < 0:
461 index = self.textbox.count()
462 self.textbox.addItem(value, pattern)
463 self.textbox.setCurrentIndex(index)
464 return
465 else:
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)
471 self.Find(direction)
473 def NotFound(self):
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):
481 self.glb = glb
482 self.params = params
483 self.row = row
484 self.parent_item = parent_item
485 self.query_done = False
486 self.child_count = 0
487 self.child_items = []
488 if parent_item:
489 self.level = parent_item.level + 1
490 else:
491 self.level = 0
493 def getChildItem(self, row):
494 return self.child_items[row]
496 def getParentItem(self):
497 return self.parent_item
499 def getRow(self):
500 return self.row
502 def childCount(self):
503 if not self.query_done:
504 self.Select()
505 if not self.child_count:
506 return -1
507 return self.child_count
509 def hasChildren(self):
510 if not self.query_done:
511 return True
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
529 self.time = time
531 def Select(self):
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)"
536 else:
537 ipc_str = ""
538 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
539 " FROM calls"
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")
548 while query.next():
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))
553 else:
554 insn_cnt = 0
555 cyc_cnt = 0
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)
567 dso = dsoname(dso)
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 ]
574 else:
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), "", "", "", "", "", "", "", "", "", "", ""]
586 else:
587 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
588 self.dbid = thread_id
590 def Select(self):
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)
603 else:
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, "", "", "", "", "", "", "", "", "", "", ""]
614 else:
615 self.data = [comm, "", "", "", "", "", ""]
616 self.dbid = comm_id
618 def Select(self):
619 self.query_done = True
620 query = QSqlQuery(self.glb.db)
621 QueryExec(query, "SELECT thread_id, pid, tid"
622 " FROM comm_threads"
623 " INNER JOIN threads ON thread_id = threads.id"
624 " WHERE comm_id = " + str(self.dbid))
625 while query.next():
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)
636 self.dbid = 0
637 self.query_done = True
638 if_has_calls = ""
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)
643 while query.next():
644 if not query.value(0):
645 continue
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):
665 if pattern:
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:
672 # Escape % and _
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) + "'"
678 else:
679 match = " GLOB '" + str(value) + "'"
680 else:
681 match = " = '" + str(value) + "'"
682 self.DoFindSelect(query, match)
684 def Found(self, query, found):
685 if found:
686 return self.FindPath(query)
687 return []
689 def FindValue(self, value, pattern, query, last_value, last_pattern):
690 if last_value == value and pattern == last_pattern:
691 found = query.first()
692 else:
693 self.FindSelect(value, pattern, query)
694 found = query.next()
695 return self.Found(query, found)
697 def FindNext(self, query):
698 found = query.next()
699 if not found:
700 found = query.first()
701 return self.Found(query, found)
703 def FindPrev(self, query):
704 found = query.previous()
705 if not found:
706 found = query.last()
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)
714 else:
715 ids = self.FindPrev(c.query)
716 return (True, ids)
718 def Find(self, value, direction, pattern, context, callback):
719 class Context():
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)
724 if len(context):
725 context[0].Update(value, direction, pattern)
726 else:
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)
731 thread.start()
733 def FindDone(self, thread, callback, ids):
734 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)
743 def GetRoot(self):
744 return CallGraphRootItem(self.glb, self.params)
746 def columnCount(self, parent=None):
747 if self.params.have_ipc:
748 return 12
749 else:
750 return 7
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 (%) "]
755 else:
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 ]
762 else:
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"
768 " FROM calls"
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 symbols.name" + match +
772 " GROUP BY comm_id, thread_id, call_path_id"
773 " ORDER BY comm_id, thread_id, call_path_id")
775 def FindPath(self, query):
776 # Turn the query result into a list of ids that the tree view can walk
777 # to open the tree at the right place.
778 ids = []
779 parent_id = query.value(0)
780 while parent_id:
781 ids.insert(0, parent_id)
782 q2 = QSqlQuery(self.glb.db)
783 QueryExec(q2, "SELECT parent_id"
784 " FROM call_paths"
785 " WHERE id = " + str(parent_id))
786 if not q2.next():
787 break
788 parent_id = q2.value(0)
789 # The call path root is not used
790 if ids[0] == 1:
791 del ids[0]
792 ids.insert(0, query.value(2))
793 ids.insert(0, query.value(1))
794 return ids
796 # Call tree data model level 2+ item base
798 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
800 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
801 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
802 self.comm_id = comm_id
803 self.thread_id = thread_id
804 self.calls_id = calls_id
805 self.call_time = call_time
806 self.time = time
807 self.insn_cnt = insn_cnt
808 self.cyc_cnt = cyc_cnt
809 self.branch_count = branch_count
811 def Select(self):
812 self.query_done = True
813 if self.calls_id == 0:
814 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
815 else:
816 comm_thread = ""
817 if self.params.have_ipc:
818 ipc_str = ", insn_count, cyc_count"
819 else:
820 ipc_str = ""
821 query = QSqlQuery(self.glb.db)
822 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
823 " FROM calls"
824 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
825 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
826 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
827 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
828 " ORDER BY call_time, calls.id")
829 while query.next():
830 if self.params.have_ipc:
831 insn_cnt = int(query.value(5))
832 cyc_cnt = int(query.value(6))
833 branch_count = int(query.value(7))
834 else:
835 insn_cnt = 0
836 cyc_cnt = 0
837 branch_count = int(query.value(5))
838 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)
839 self.child_items.append(child_item)
840 self.child_count += 1
842 # Call tree data model level three item
844 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
846 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):
847 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
848 dso = dsoname(dso)
849 if self.params.have_ipc:
850 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
851 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
852 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
853 ipc = CalcIPC(cyc_cnt, insn_cnt)
854 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 ]
855 else:
856 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
857 self.dbid = calls_id
859 # Call tree data model level two item
861 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
863 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
864 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
865 if self.params.have_ipc:
866 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
867 else:
868 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
869 self.dbid = thread_id
871 def Select(self):
872 super(CallTreeLevelTwoItem, self).Select()
873 for child_item in self.child_items:
874 self.time += child_item.time
875 self.insn_cnt += child_item.insn_cnt
876 self.cyc_cnt += child_item.cyc_cnt
877 self.branch_count += child_item.branch_count
878 for child_item in self.child_items:
879 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
880 if self.params.have_ipc:
881 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
882 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
883 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
884 else:
885 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
887 # Call tree data model level one item
889 class CallTreeLevelOneItem(CallGraphLevelItemBase):
891 def __init__(self, glb, params, row, comm_id, comm, parent_item):
892 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
893 if self.params.have_ipc:
894 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
895 else:
896 self.data = [comm, "", "", "", "", "", ""]
897 self.dbid = comm_id
899 def Select(self):
900 self.query_done = True
901 query = QSqlQuery(self.glb.db)
902 QueryExec(query, "SELECT thread_id, pid, tid"
903 " FROM comm_threads"
904 " INNER JOIN threads ON thread_id = threads.id"
905 " WHERE comm_id = " + str(self.dbid))
906 while query.next():
907 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
908 self.child_items.append(child_item)
909 self.child_count += 1
911 # Call tree data model root item
913 class CallTreeRootItem(CallGraphLevelItemBase):
915 def __init__(self, glb, params):
916 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
917 self.dbid = 0
918 self.query_done = True
919 if_has_calls = ""
920 if IsSelectable(glb.db, "comms", columns = "has_calls"):
921 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
922 query = QSqlQuery(glb.db)
923 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
924 while query.next():
925 if not query.value(0):
926 continue
927 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
928 self.child_items.append(child_item)
929 self.child_count += 1
931 # Call Tree data model
933 class CallTreeModel(CallGraphModelBase):
935 def __init__(self, glb, parent=None):
936 super(CallTreeModel, self).__init__(glb, parent)
938 def GetRoot(self):
939 return CallTreeRootItem(self.glb, self.params)
941 def columnCount(self, parent=None):
942 if self.params.have_ipc:
943 return 12
944 else:
945 return 7
947 def columnHeader(self, column):
948 if self.params.have_ipc:
949 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
950 else:
951 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
952 return headers[column]
954 def columnAlignment(self, column):
955 if self.params.have_ipc:
956 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 ]
957 else:
958 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
959 return alignment[column]
961 def DoFindSelect(self, query, match):
962 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
963 " FROM calls"
964 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
965 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
966 " WHERE symbols.name" + match +
967 " ORDER BY comm_id, thread_id, call_time, calls.id")
969 def FindPath(self, query):
970 # Turn the query result into a list of ids that the tree view can walk
971 # to open the tree at the right place.
972 ids = []
973 parent_id = query.value(0)
974 while parent_id:
975 ids.insert(0, parent_id)
976 q2 = QSqlQuery(self.glb.db)
977 QueryExec(q2, "SELECT parent_id"
978 " FROM calls"
979 " WHERE id = " + str(parent_id))
980 if not q2.next():
981 break
982 parent_id = q2.value(0)
983 ids.insert(0, query.value(2))
984 ids.insert(0, query.value(1))
985 return ids
987 # Vertical layout
989 class HBoxLayout(QHBoxLayout):
991 def __init__(self, *children):
992 super(HBoxLayout, self).__init__()
994 self.layout().setContentsMargins(0, 0, 0, 0)
995 for child in children:
996 if child.isWidgetType():
997 self.layout().addWidget(child)
998 else:
999 self.layout().addLayout(child)
1001 # Horizontal layout
1003 class VBoxLayout(QVBoxLayout):
1005 def __init__(self, *children):
1006 super(VBoxLayout, self).__init__()
1008 self.layout().setContentsMargins(0, 0, 0, 0)
1009 for child in children:
1010 if child.isWidgetType():
1011 self.layout().addWidget(child)
1012 else:
1013 self.layout().addLayout(child)
1015 # Vertical layout widget
1017 class VBox():
1019 def __init__(self, *children):
1020 self.vbox = QWidget()
1021 self.vbox.setLayout(VBoxLayout(*children))
1023 def Widget(self):
1024 return self.vbox
1026 # Tree window base
1028 class TreeWindowBase(QMdiSubWindow):
1030 def __init__(self, parent=None):
1031 super(TreeWindowBase, self).__init__(parent)
1033 self.model = None
1034 self.find_bar = None
1036 self.view = QTreeView()
1037 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1038 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1040 self.context_menu = TreeContextMenu(self.view)
1042 def DisplayFound(self, ids):
1043 if not len(ids):
1044 return False
1045 parent = QModelIndex()
1046 for dbid in ids:
1047 found = False
1048 n = self.model.rowCount(parent)
1049 for row in xrange(n):
1050 child = self.model.index(row, 0, parent)
1051 if child.internalPointer().dbid == dbid:
1052 found = True
1053 self.view.setCurrentIndex(child)
1054 parent = child
1055 break
1056 if not found:
1057 break
1058 return found
1060 def Find(self, value, direction, pattern, context):
1061 self.view.setFocus()
1062 self.find_bar.Busy()
1063 self.model.Find(value, direction, pattern, context, self.FindDone)
1065 def FindDone(self, ids):
1066 found = True
1067 if not self.DisplayFound(ids):
1068 found = False
1069 self.find_bar.Idle()
1070 if not found:
1071 self.find_bar.NotFound()
1074 # Context-sensitive call graph window
1076 class CallGraphWindow(TreeWindowBase):
1078 def __init__(self, glb, parent=None):
1079 super(CallGraphWindow, self).__init__(parent)
1081 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1083 self.view.setModel(self.model)
1085 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1086 self.view.setColumnWidth(c, w)
1088 self.find_bar = FindBar(self, self)
1090 self.vbox = VBox(self.view, self.find_bar.Widget())
1092 self.setWidget(self.vbox.Widget())
1094 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1096 # Call tree window
1098 class CallTreeWindow(TreeWindowBase):
1100 def __init__(self, glb, parent=None, thread_at_time=None):
1101 super(CallTreeWindow, self).__init__(parent)
1103 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1105 self.view.setModel(self.model)
1107 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1108 self.view.setColumnWidth(c, w)
1110 self.find_bar = FindBar(self, self)
1112 self.vbox = VBox(self.view, self.find_bar.Widget())
1114 self.setWidget(self.vbox.Widget())
1116 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1118 if thread_at_time:
1119 self.DisplayThreadAtTime(*thread_at_time)
1121 def DisplayThreadAtTime(self, comm_id, thread_id, time):
1122 parent = QModelIndex()
1123 for dbid in (comm_id, thread_id):
1124 found = False
1125 n = self.model.rowCount(parent)
1126 for row in xrange(n):
1127 child = self.model.index(row, 0, parent)
1128 if child.internalPointer().dbid == dbid:
1129 found = True
1130 self.view.setCurrentIndex(child)
1131 parent = child
1132 break
1133 if not found:
1134 return
1135 found = False
1136 while True:
1137 n = self.model.rowCount(parent)
1138 if not n:
1139 return
1140 last_child = None
1141 for row in xrange(n):
1142 child = self.model.index(row, 0, parent)
1143 child_call_time = child.internalPointer().call_time
1144 if child_call_time < time:
1145 last_child = child
1146 elif child_call_time == time:
1147 self.view.setCurrentIndex(child)
1148 return
1149 elif child_call_time > time:
1150 break
1151 if not last_child:
1152 if not found:
1153 child = self.model.index(0, 0, parent)
1154 self.view.setCurrentIndex(child)
1155 return
1156 found = True
1157 self.view.setCurrentIndex(last_child)
1158 parent = last_child
1160 # ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1162 def ExecComm(db, thread_id, time):
1163 query = QSqlQuery(db)
1164 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1165 " FROM comm_threads"
1166 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1167 " WHERE comm_threads.thread_id = " + str(thread_id) +
1168 " ORDER BY comms.c_time, comms.id")
1169 first = None
1170 last = None
1171 while query.next():
1172 if first is None:
1173 first = query.value(0)
1174 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1175 last = query.value(0)
1176 if not(last is None):
1177 return last
1178 return first
1180 # Container for (x, y) data
1182 class XY():
1183 def __init__(self, x=0, y=0):
1184 self.x = x
1185 self.y = y
1187 def __str__(self):
1188 return "XY({}, {})".format(str(self.x), str(self.y))
1190 # Container for sub-range data
1192 class Subrange():
1193 def __init__(self, lo=0, hi=0):
1194 self.lo = lo
1195 self.hi = hi
1197 def __str__(self):
1198 return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1200 # Graph data region base class
1202 class GraphDataRegion(object):
1204 def __init__(self, key, title = "", ordinal = ""):
1205 self.key = key
1206 self.title = title
1207 self.ordinal = ordinal
1209 # Function to sort GraphDataRegion
1211 def GraphDataRegionOrdinal(data_region):
1212 return data_region.ordinal
1214 # Attributes for a graph region
1216 class GraphRegionAttribute():
1218 def __init__(self, colour):
1219 self.colour = colour
1221 # Switch graph data region represents a task
1223 class SwitchGraphDataRegion(GraphDataRegion):
1225 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1226 super(SwitchGraphDataRegion, self).__init__(key)
1228 self.title = str(pid) + " / " + str(tid) + " " + comm
1229 # Order graph legend within exec comm by pid / tid / time
1230 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1231 self.exec_comm_id = exec_comm_id
1232 self.pid = pid
1233 self.tid = tid
1234 self.comm = comm
1235 self.thread_id = thread_id
1236 self.comm_id = comm_id
1238 # Graph data point
1240 class GraphDataPoint():
1242 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1243 self.data = data
1244 self.index = index
1245 self.x = x
1246 self.y = y
1247 self.altx = altx
1248 self.alty = alty
1249 self.hregion = hregion
1250 self.vregion = vregion
1252 # Graph data (single graph) base class
1254 class GraphData(object):
1256 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1257 self.collection = collection
1258 self.points = []
1259 self.xbase = xbase
1260 self.ybase = ybase
1261 self.title = ""
1263 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1264 index = len(self.points)
1266 x = float(Decimal(x) - self.xbase)
1267 y = float(Decimal(y) - self.ybase)
1269 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1271 def XToData(self, x):
1272 return Decimal(x) + self.xbase
1274 def YToData(self, y):
1275 return Decimal(y) + self.ybase
1277 # Switch graph data (for one CPU)
1279 class SwitchGraphData(GraphData):
1281 def __init__(self, db, collection, cpu, xbase):
1282 super(SwitchGraphData, self).__init__(collection, xbase)
1284 self.cpu = cpu
1285 self.title = "CPU " + str(cpu)
1286 self.SelectSwitches(db)
1288 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1289 query = QSqlQuery(db)
1290 QueryExec(query, "SELECT id, c_time"
1291 " FROM comms"
1292 " WHERE c_thread_id = " + str(thread_id) +
1293 " AND exec_flag = " + self.collection.glb.dbref.TRUE +
1294 " AND c_time >= " + str(start_time) +
1295 " AND c_time <= " + str(end_time) +
1296 " ORDER BY c_time, id")
1297 while query.next():
1298 comm_id = query.value(0)
1299 if comm_id == last_comm_id:
1300 continue
1301 time = query.value(1)
1302 hregion = self.HRegion(db, thread_id, comm_id, time)
1303 self.AddPoint(time, 1000, None, None, hregion)
1305 def SelectSwitches(self, db):
1306 last_time = None
1307 last_comm_id = None
1308 last_thread_id = None
1309 query = QSqlQuery(db)
1310 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1311 " FROM context_switches"
1312 " WHERE machine_id = " + str(self.collection.machine_id) +
1313 " AND cpu = " + str(self.cpu) +
1314 " ORDER BY time, id")
1315 while query.next():
1316 flags = int(query.value(5))
1317 if flags & 1:
1318 # Schedule-out: detect and add exec's
1319 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1320 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1321 continue
1322 # Schedule-in: add data point
1323 if len(self.points) == 0:
1324 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1325 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1326 self.AddPoint(start_time, 1000, None, None, hregion)
1327 time = query.value(0)
1328 comm_id = query.value(4)
1329 thread_id = query.value(2)
1330 hregion = self.HRegion(db, thread_id, comm_id, time)
1331 self.AddPoint(time, 1000, None, None, hregion)
1332 last_time = time
1333 last_comm_id = comm_id
1334 last_thread_id = thread_id
1336 def NewHRegion(self, db, key, thread_id, comm_id, time):
1337 exec_comm_id = ExecComm(db, thread_id, time)
1338 query = QSqlQuery(db)
1339 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1340 if query.next():
1341 pid = query.value(0)
1342 tid = query.value(1)
1343 else:
1344 pid = -1
1345 tid = -1
1346 query = QSqlQuery(db)
1347 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1348 if query.next():
1349 comm = query.value(0)
1350 else:
1351 comm = ""
1352 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1354 def HRegion(self, db, thread_id, comm_id, time):
1355 key = str(thread_id) + ":" + str(comm_id)
1356 hregion = self.collection.LookupHRegion(key)
1357 if hregion is None:
1358 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1359 self.collection.AddHRegion(key, hregion)
1360 return hregion
1362 # Graph data collection (multiple related graphs) base class
1364 class GraphDataCollection(object):
1366 def __init__(self, glb):
1367 self.glb = glb
1368 self.data = []
1369 self.hregions = {}
1370 self.xrangelo = None
1371 self.xrangehi = None
1372 self.yrangelo = None
1373 self.yrangehi = None
1374 self.dp = XY(0, 0)
1376 def AddGraphData(self, data):
1377 self.data.append(data)
1379 def LookupHRegion(self, key):
1380 if key in self.hregions:
1381 return self.hregions[key]
1382 return None
1384 def AddHRegion(self, key, hregion):
1385 self.hregions[key] = hregion
1387 # Switch graph data collection (SwitchGraphData for each CPU)
1389 class SwitchGraphDataCollection(GraphDataCollection):
1391 def __init__(self, glb, db, machine_id):
1392 super(SwitchGraphDataCollection, self).__init__(glb)
1394 self.machine_id = machine_id
1395 self.cpus = self.SelectCPUs(db)
1397 self.xrangelo = glb.StartTime(machine_id)
1398 self.xrangehi = glb.FinishTime(machine_id)
1400 self.yrangelo = Decimal(0)
1401 self.yrangehi = Decimal(1000)
1403 for cpu in self.cpus:
1404 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1406 def SelectCPUs(self, db):
1407 cpus = []
1408 query = QSqlQuery(db)
1409 QueryExec(query, "SELECT DISTINCT cpu"
1410 " FROM context_switches"
1411 " WHERE machine_id = " + str(self.machine_id))
1412 while query.next():
1413 cpus.append(int(query.value(0)))
1414 return sorted(cpus)
1416 # Switch graph data graphics item displays the graphed data
1418 class SwitchGraphDataGraphicsItem(QGraphicsItem):
1420 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1421 super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1423 self.data = data
1424 self.graph_width = graph_width
1425 self.graph_height = graph_height
1426 self.attrs = attrs
1427 self.event_handler = event_handler
1428 self.setAcceptHoverEvents(True)
1430 def boundingRect(self):
1431 return QRectF(0, 0, self.graph_width, self.graph_height)
1433 def PaintPoint(self, painter, last, x):
1434 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1435 if last.x < self.attrs.subrange.x.lo:
1436 x0 = self.attrs.subrange.x.lo
1437 else:
1438 x0 = last.x
1439 if x > self.attrs.subrange.x.hi:
1440 x1 = self.attrs.subrange.x.hi
1441 else:
1442 x1 = x - 1
1443 x0 = self.attrs.XToPixel(x0)
1444 x1 = self.attrs.XToPixel(x1)
1446 y0 = self.attrs.YToPixel(last.y)
1448 colour = self.attrs.region_attributes[last.hregion.key].colour
1450 width = x1 - x0 + 1
1451 if width < 2:
1452 painter.setPen(colour)
1453 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1454 else:
1455 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1457 def paint(self, painter, option, widget):
1458 last = None
1459 for point in self.data.points:
1460 self.PaintPoint(painter, last, point.x)
1461 if point.x > self.attrs.subrange.x.hi:
1462 break;
1463 last = point
1464 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1466 def BinarySearchPoint(self, target):
1467 lower_pos = 0
1468 higher_pos = len(self.data.points)
1469 while True:
1470 pos = int((lower_pos + higher_pos) / 2)
1471 val = self.data.points[pos].x
1472 if target >= val:
1473 lower_pos = pos
1474 else:
1475 higher_pos = pos
1476 if higher_pos <= lower_pos + 1:
1477 return lower_pos
1479 def XPixelToData(self, x):
1480 x = self.attrs.PixelToX(x)
1481 if x < self.data.points[0].x:
1482 x = 0
1483 pos = 0
1484 low = True
1485 else:
1486 pos = self.BinarySearchPoint(x)
1487 low = False
1488 return (low, pos, self.data.XToData(x))
1490 def EventToData(self, event):
1491 no_data = (None,) * 4
1492 if len(self.data.points) < 1:
1493 return no_data
1494 x = event.pos().x()
1495 if x < 0:
1496 return no_data
1497 low0, pos0, time_from = self.XPixelToData(x)
1498 low1, pos1, time_to = self.XPixelToData(x + 1)
1499 hregions = set()
1500 hregion_times = []
1501 if not low1:
1502 for i in xrange(pos0, pos1 + 1):
1503 hregion = self.data.points[i].hregion
1504 hregions.add(hregion)
1505 if i == pos0:
1506 time = time_from
1507 else:
1508 time = self.data.XToData(self.data.points[i].x)
1509 hregion_times.append((hregion, time))
1510 return (time_from, time_to, hregions, hregion_times)
1512 def hoverMoveEvent(self, event):
1513 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1514 if time_from is not None:
1515 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1517 def hoverLeaveEvent(self, event):
1518 self.event_handler.NoPointEvent()
1520 def mousePressEvent(self, event):
1521 if event.button() != Qt.RightButton:
1522 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1523 return
1524 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1525 if hregion_times:
1526 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1528 # X-axis graphics item
1530 class XAxisGraphicsItem(QGraphicsItem):
1532 def __init__(self, width, parent=None):
1533 super(XAxisGraphicsItem, self).__init__(parent)
1535 self.width = width
1536 self.max_mark_sz = 4
1537 self.height = self.max_mark_sz + 1
1539 def boundingRect(self):
1540 return QRectF(0, 0, self.width, self.height)
1542 def Step(self):
1543 attrs = self.parentItem().attrs
1544 subrange = attrs.subrange.x
1545 t = subrange.hi - subrange.lo
1546 s = (3.0 * t) / self.width
1547 n = 1.0
1548 while s > n:
1549 n = n * 10.0
1550 return n
1552 def PaintMarks(self, painter, at_y, lo, hi, step, i):
1553 attrs = self.parentItem().attrs
1554 x = lo
1555 while x <= hi:
1556 xp = attrs.XToPixel(x)
1557 if i % 10:
1558 if i % 5:
1559 sz = 1
1560 else:
1561 sz = 2
1562 else:
1563 sz = self.max_mark_sz
1564 i = 0
1565 painter.drawLine(xp, at_y, xp, at_y + sz)
1566 x += step
1567 i += 1
1569 def paint(self, painter, option, widget):
1570 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1571 painter.drawLine(0, 0, self.width - 1, 0)
1572 n = self.Step()
1573 attrs = self.parentItem().attrs
1574 subrange = attrs.subrange.x
1575 if subrange.lo:
1576 x_offset = n - (subrange.lo % n)
1577 else:
1578 x_offset = 0.0
1579 x = subrange.lo + x_offset
1580 i = (x / n) % 10
1581 self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1583 def ScaleDimensions(self):
1584 n = self.Step()
1585 attrs = self.parentItem().attrs
1586 lo = attrs.subrange.x.lo
1587 hi = (n * 10.0) + lo
1588 width = attrs.XToPixel(hi)
1589 if width > 500:
1590 width = 0
1591 return (n, lo, hi, width)
1593 def PaintScale(self, painter, at_x, at_y):
1594 n, lo, hi, width = self.ScaleDimensions()
1595 if not width:
1596 return
1597 painter.drawLine(at_x, at_y, at_x + width, at_y)
1598 self.PaintMarks(painter, at_y, lo, hi, n, 0)
1600 def ScaleWidth(self):
1601 n, lo, hi, width = self.ScaleDimensions()
1602 return width
1604 def ScaleHeight(self):
1605 return self.height
1607 def ScaleUnit(self):
1608 return self.Step() * 10
1610 # Scale graphics item base class
1612 class ScaleGraphicsItem(QGraphicsItem):
1614 def __init__(self, axis, parent=None):
1615 super(ScaleGraphicsItem, self).__init__(parent)
1616 self.axis = axis
1618 def boundingRect(self):
1619 scale_width = self.axis.ScaleWidth()
1620 if not scale_width:
1621 return QRectF()
1622 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1624 def paint(self, painter, option, widget):
1625 scale_width = self.axis.ScaleWidth()
1626 if not scale_width:
1627 return
1628 self.axis.PaintScale(painter, 0, 5)
1629 x = scale_width + 4
1630 painter.drawText(QPointF(x, 10), self.Text())
1632 def Unit(self):
1633 return self.axis.ScaleUnit()
1635 def Text(self):
1636 return ""
1638 # Switch graph scale graphics item
1640 class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1642 def __init__(self, axis, parent=None):
1643 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1645 def Text(self):
1646 unit = self.Unit()
1647 if unit >= 1000000000:
1648 unit = int(unit / 1000000000)
1649 us = "s"
1650 elif unit >= 1000000:
1651 unit = int(unit / 1000000)
1652 us = "ms"
1653 elif unit >= 1000:
1654 unit = int(unit / 1000)
1655 us = "us"
1656 else:
1657 unit = int(unit)
1658 us = "ns"
1659 return " = " + str(unit) + " " + us
1661 # Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1663 class SwitchGraphGraphicsItem(QGraphicsItem):
1665 def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1666 super(SwitchGraphGraphicsItem, self).__init__(parent)
1667 self.collection = collection
1668 self.data = data
1669 self.attrs = attrs
1670 self.event_handler = event_handler
1672 margin = 20
1673 title_width = 50
1675 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1677 self.title_graphics.setPos(margin, margin)
1678 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1679 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1681 self.graph_origin_x = margin + title_width + margin
1682 self.graph_origin_y = graph_height + margin
1684 x_axis_size = 1
1685 y_axis_size = 1
1686 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1688 self.x_axis = XAxisGraphicsItem(graph_width, self)
1689 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1691 if first:
1692 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1693 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1695 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1697 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1698 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1700 self.width = self.graph_origin_x + graph_width + margin
1701 self.height = self.graph_origin_y + margin
1703 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1704 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1706 if parent and 'EnableRubberBand' in dir(parent):
1707 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1709 def boundingRect(self):
1710 return QRectF(0, 0, self.width, self.height)
1712 def paint(self, painter, option, widget):
1713 pass
1715 def RBXToPixel(self, x):
1716 return self.attrs.PixelToX(x - self.graph_origin_x)
1718 def RBXRangeToPixel(self, x0, x1):
1719 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1721 def RBPixelToTime(self, x):
1722 if x < self.data.points[0].x:
1723 return self.data.XToData(0)
1724 return self.data.XToData(x)
1726 def RBEventTimes(self, x0, x1):
1727 x0, x1 = self.RBXRangeToPixel(x0, x1)
1728 time_from = self.RBPixelToTime(x0)
1729 time_to = self.RBPixelToTime(x1)
1730 return (time_from, time_to)
1732 def RBEvent(self, x0, x1):
1733 time_from, time_to = self.RBEventTimes(x0, x1)
1734 self.event_handler.RangeEvent(time_from, time_to)
1736 def RBMoveEvent(self, x0, x1):
1737 if x1 < x0:
1738 x0, x1 = x1, x0
1739 self.RBEvent(x0, x1)
1741 def RBReleaseEvent(self, x0, x1, selection_state):
1742 if x1 < x0:
1743 x0, x1 = x1, x0
1744 x0, x1 = self.RBXRangeToPixel(x0, x1)
1745 self.event_handler.SelectEvent(x0, x1, selection_state)
1747 # Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1749 class VerticalBracketGraphicsItem(QGraphicsItem):
1751 def __init__(self, parent=None):
1752 super(VerticalBracketGraphicsItem, self).__init__(parent)
1754 self.width = 0
1755 self.height = 0
1756 self.hide()
1758 def SetSize(self, width, height):
1759 self.width = width + 1
1760 self.height = height + 1
1762 def boundingRect(self):
1763 return QRectF(0, 0, self.width, self.height)
1765 def paint(self, painter, option, widget):
1766 colour = QColor(255, 255, 0, 32)
1767 painter.fillRect(0, 0, self.width, self.height, colour)
1768 x1 = self.width - 1
1769 y1 = self.height - 1
1770 painter.drawLine(0, 0, x1, 0)
1771 painter.drawLine(0, 0, 0, 3)
1772 painter.drawLine(x1, 0, x1, 3)
1773 painter.drawLine(0, y1, x1, y1)
1774 painter.drawLine(0, y1, 0, y1 - 3)
1775 painter.drawLine(x1, y1, x1, y1 - 3)
1777 # Graphics item to contain graphs arranged vertically
1779 class VertcalGraphSetGraphicsItem(QGraphicsItem):
1781 def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1782 super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1784 self.collection = collection
1786 self.top = 10
1788 self.width = 0
1789 self.height = self.top
1791 self.rubber_band = None
1792 self.rb_enabled = False
1794 first = True
1795 for data in collection.data:
1796 child = child_class(collection, data, attrs, event_handler, first, self)
1797 child.setPos(0, self.height + 1)
1798 rect = child.boundingRect()
1799 if rect.right() > self.width:
1800 self.width = rect.right()
1801 self.height = self.height + rect.bottom() + 1
1802 first = False
1804 self.bracket = VerticalBracketGraphicsItem(self)
1806 def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1807 if self.rb_enabled:
1808 return
1809 self.rb_enabled = True
1810 self.rb_in_view = False
1811 self.setAcceptedMouseButtons(Qt.LeftButton)
1812 self.rb_xlo = xlo
1813 self.rb_xhi = xhi
1814 self.rb_event_handler = rb_event_handler
1815 self.mousePressEvent = self.MousePressEvent
1816 self.mouseMoveEvent = self.MouseMoveEvent
1817 self.mouseReleaseEvent = self.MouseReleaseEvent
1819 def boundingRect(self):
1820 return QRectF(0, 0, self.width, self.height)
1822 def paint(self, painter, option, widget):
1823 pass
1825 def RubberBandParent(self):
1826 scene = self.scene()
1827 view = scene.views()[0]
1828 viewport = view.viewport()
1829 return viewport
1831 def RubberBandSetGeometry(self, rect):
1832 scene_rectf = self.mapRectToScene(QRectF(rect))
1833 scene = self.scene()
1834 view = scene.views()[0]
1835 poly = view.mapFromScene(scene_rectf)
1836 self.rubber_band.setGeometry(poly.boundingRect())
1838 def SetSelection(self, selection_state):
1839 if self.rubber_band:
1840 if selection_state:
1841 self.RubberBandSetGeometry(selection_state)
1842 self.rubber_band.show()
1843 else:
1844 self.rubber_band.hide()
1846 def SetBracket(self, rect):
1847 if rect:
1848 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1849 self.bracket.setPos(x, y)
1850 self.bracket.SetSize(width, height)
1851 self.bracket.show()
1852 else:
1853 self.bracket.hide()
1855 def RubberBandX(self, event):
1856 x = event.pos().toPoint().x()
1857 if x < self.rb_xlo:
1858 x = self.rb_xlo
1859 elif x > self.rb_xhi:
1860 x = self.rb_xhi
1861 else:
1862 self.rb_in_view = True
1863 return x
1865 def RubberBandRect(self, x):
1866 if self.rb_origin.x() <= x:
1867 width = x - self.rb_origin.x()
1868 rect = QRect(self.rb_origin, QSize(width, self.height))
1869 else:
1870 width = self.rb_origin.x() - x
1871 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1872 rect = QRect(top_left, QSize(width, self.height))
1873 return rect
1875 def MousePressEvent(self, event):
1876 self.rb_in_view = False
1877 x = self.RubberBandX(event)
1878 self.rb_origin = QPoint(x, self.top)
1879 if self.rubber_band is None:
1880 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1881 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1882 if self.rb_in_view:
1883 self.rubber_band.show()
1884 self.rb_event_handler.RBMoveEvent(x, x)
1885 else:
1886 self.rubber_band.hide()
1888 def MouseMoveEvent(self, event):
1889 x = self.RubberBandX(event)
1890 rect = self.RubberBandRect(x)
1891 self.RubberBandSetGeometry(rect)
1892 if self.rb_in_view:
1893 self.rubber_band.show()
1894 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1896 def MouseReleaseEvent(self, event):
1897 x = self.RubberBandX(event)
1898 if self.rb_in_view:
1899 selection_state = self.RubberBandRect(x)
1900 else:
1901 selection_state = None
1902 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1904 # Switch graph legend data model
1906 class SwitchGraphLegendModel(QAbstractTableModel):
1908 def __init__(self, collection, region_attributes, parent=None):
1909 super(SwitchGraphLegendModel, self).__init__(parent)
1911 self.region_attributes = region_attributes
1913 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1914 self.child_count = len(self.child_items)
1916 self.highlight_set = set()
1918 self.column_headers = ("pid", "tid", "comm")
1920 def rowCount(self, parent):
1921 return self.child_count
1923 def headerData(self, section, orientation, role):
1924 if role != Qt.DisplayRole:
1925 return None
1926 if orientation != Qt.Horizontal:
1927 return None
1928 return self.columnHeader(section)
1930 def index(self, row, column, parent):
1931 return self.createIndex(row, column, self.child_items[row])
1933 def columnCount(self, parent=None):
1934 return len(self.column_headers)
1936 def columnHeader(self, column):
1937 return self.column_headers[column]
1939 def data(self, index, role):
1940 if role == Qt.BackgroundRole:
1941 child = self.child_items[index.row()]
1942 if child in self.highlight_set:
1943 return self.region_attributes[child.key].colour
1944 return None
1945 if role == Qt.ForegroundRole:
1946 child = self.child_items[index.row()]
1947 if child in self.highlight_set:
1948 return QColor(255, 255, 255)
1949 return self.region_attributes[child.key].colour
1950 if role != Qt.DisplayRole:
1951 return None
1952 hregion = self.child_items[index.row()]
1953 col = index.column()
1954 if col == 0:
1955 return hregion.pid
1956 if col == 1:
1957 return hregion.tid
1958 if col == 2:
1959 return hregion.comm
1960 return None
1962 def SetHighlight(self, row, set_highlight):
1963 child = self.child_items[row]
1964 top_left = self.createIndex(row, 0, child)
1965 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1966 self.dataChanged.emit(top_left, bottom_right)
1968 def Highlight(self, highlight_set):
1969 for row in xrange(self.child_count):
1970 child = self.child_items[row]
1971 if child in self.highlight_set:
1972 if child not in highlight_set:
1973 self.SetHighlight(row, False)
1974 elif child in highlight_set:
1975 self.SetHighlight(row, True)
1976 self.highlight_set = highlight_set
1978 # Switch graph legend is a table
1980 class SwitchGraphLegend(QWidget):
1982 def __init__(self, collection, region_attributes, parent=None):
1983 super(SwitchGraphLegend, self).__init__(parent)
1985 self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1987 self.model = QSortFilterProxyModel()
1988 self.model.setSourceModel(self.data_model)
1990 self.view = QTableView()
1991 self.view.setModel(self.model)
1992 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
1993 self.view.verticalHeader().setVisible(False)
1994 self.view.sortByColumn(-1, Qt.AscendingOrder)
1995 self.view.setSortingEnabled(True)
1996 self.view.resizeColumnsToContents()
1997 self.view.resizeRowsToContents()
1999 self.vbox = VBoxLayout(self.view)
2000 self.setLayout(self.vbox)
2002 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2003 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2004 self.saved_size = sz1
2006 def resizeEvent(self, event):
2007 self.saved_size = self.size().width()
2008 super(SwitchGraphLegend, self).resizeEvent(event)
2010 def Highlight(self, highlight_set):
2011 self.data_model.Highlight(highlight_set)
2012 self.update()
2014 def changeEvent(self, event):
2015 if event.type() == QEvent.FontChange:
2016 self.view.resizeRowsToContents()
2017 self.view.resizeColumnsToContents()
2018 # Need to resize rows again after column resize
2019 self.view.resizeRowsToContents()
2020 super(SwitchGraphLegend, self).changeEvent(event)
2022 # Random colour generation
2024 def RGBColourTooLight(r, g, b):
2025 if g > 230:
2026 return True
2027 if g <= 160:
2028 return False
2029 if r <= 180 and g <= 180:
2030 return False
2031 if r < 60:
2032 return False
2033 return True
2035 def GenerateColours(x):
2036 cs = [0]
2037 for i in xrange(1, x):
2038 cs.append(int((255.0 / i) + 0.5))
2039 colours = []
2040 for r in cs:
2041 for g in cs:
2042 for b in cs:
2043 # Exclude black and colours that look too light against a white background
2044 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2045 continue
2046 colours.append(QColor(r, g, b))
2047 return colours
2049 def GenerateNColours(n):
2050 for x in xrange(2, n + 2):
2051 colours = GenerateColours(x)
2052 if len(colours) >= n:
2053 return colours
2054 return []
2056 def GenerateNRandomColours(n, seed):
2057 colours = GenerateNColours(n)
2058 random.seed(seed)
2059 random.shuffle(colours)
2060 return colours
2062 # Graph attributes, in particular the scale and subrange that change when zooming
2064 class GraphAttributes():
2066 def __init__(self, scale, subrange, region_attributes, dp):
2067 self.scale = scale
2068 self.subrange = subrange
2069 self.region_attributes = region_attributes
2070 # Rounding avoids errors due to finite floating point precision
2071 self.dp = dp # data decimal places
2072 self.Update()
2074 def XToPixel(self, x):
2075 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2077 def YToPixel(self, y):
2078 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2080 def PixelToXRounded(self, px):
2081 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2083 def PixelToYRounded(self, py):
2084 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2086 def PixelToX(self, px):
2087 x = self.PixelToXRounded(px)
2088 if self.pdp.x == 0:
2089 rt = self.XToPixel(x)
2090 if rt > px:
2091 return x - 1
2092 return x
2094 def PixelToY(self, py):
2095 y = self.PixelToYRounded(py)
2096 if self.pdp.y == 0:
2097 rt = self.YToPixel(y)
2098 if rt > py:
2099 return y - 1
2100 return y
2102 def ToPDP(self, dp, scale):
2103 # Calculate pixel decimal places:
2104 # (10 ** dp) is the minimum delta in the data
2105 # scale it to get the minimum delta in pixels
2106 # log10 gives the number of decimals places negatively
2107 # subtrace 1 to divide by 10
2108 # round to the lower negative number
2109 # change the sign to get the number of decimals positively
2110 x = math.log10((10 ** dp) * scale)
2111 if x < 0:
2112 x -= 1
2113 x = -int(math.floor(x) - 0.1)
2114 else:
2115 x = 0
2116 return x
2118 def Update(self):
2119 x = self.ToPDP(self.dp.x, self.scale.x)
2120 y = self.ToPDP(self.dp.y, self.scale.y)
2121 self.pdp = XY(x, y) # pixel decimal places
2123 # Switch graph splitter which divides the CPU graphs from the legend
2125 class SwitchGraphSplitter(QSplitter):
2127 def __init__(self, parent=None):
2128 super(SwitchGraphSplitter, self).__init__(parent)
2130 self.first_time = False
2132 def resizeEvent(self, ev):
2133 if self.first_time:
2134 self.first_time = False
2135 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2136 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2137 sz0 = self.size().width() - self.handleWidth() - sz1
2138 self.setSizes([sz0, sz1])
2139 elif not(self.widget(1).saved_size is None):
2140 sz1 = self.widget(1).saved_size
2141 sz0 = self.size().width() - self.handleWidth() - sz1
2142 self.setSizes([sz0, sz1])
2143 super(SwitchGraphSplitter, self).resizeEvent(ev)
2145 # Graph widget base class
2147 class GraphWidget(QWidget):
2149 graph_title_changed = Signal(object)
2151 def __init__(self, parent=None):
2152 super(GraphWidget, self).__init__(parent)
2154 def GraphTitleChanged(self, title):
2155 self.graph_title_changed.emit(title)
2157 def Title(self):
2158 return ""
2160 # Display time in s, ms, us or ns
2162 def ToTimeStr(val):
2163 val = Decimal(val)
2164 if val >= 1000000000:
2165 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2166 if val >= 1000000:
2167 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2168 if val >= 1000:
2169 return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2170 return "{} ns".format(val.quantize(Decimal("1")))
2172 # Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2174 class SwitchGraphWidget(GraphWidget):
2176 def __init__(self, glb, collection, parent=None):
2177 super(SwitchGraphWidget, self).__init__(parent)
2179 self.glb = glb
2180 self.collection = collection
2182 self.back_state = []
2183 self.forward_state = []
2184 self.selection_state = (None, None)
2185 self.fwd_rect = None
2186 self.start_time = self.glb.StartTime(collection.machine_id)
2188 i = 0
2189 hregions = collection.hregions.values()
2190 colours = GenerateNRandomColours(len(hregions), 1013)
2191 region_attributes = {}
2192 for hregion in hregions:
2193 if hregion.pid == 0 and hregion.tid == 0:
2194 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2195 else:
2196 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2197 i = i + 1
2199 # Default to entire range
2200 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2201 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2202 subrange = XY(xsubrange, ysubrange)
2204 scale = self.GetScaleForRange(subrange)
2206 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2208 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2210 self.scene = QGraphicsScene()
2211 self.scene.addItem(self.item)
2213 self.view = QGraphicsView(self.scene)
2214 self.view.centerOn(0, 0)
2215 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2217 self.legend = SwitchGraphLegend(collection, region_attributes)
2219 self.splitter = SwitchGraphSplitter()
2220 self.splitter.addWidget(self.view)
2221 self.splitter.addWidget(self.legend)
2223 self.point_label = QLabel("")
2224 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2226 self.back_button = QToolButton()
2227 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2228 self.back_button.setDisabled(True)
2229 self.back_button.released.connect(lambda: self.Back())
2231 self.forward_button = QToolButton()
2232 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2233 self.forward_button.setDisabled(True)
2234 self.forward_button.released.connect(lambda: self.Forward())
2236 self.zoom_button = QToolButton()
2237 self.zoom_button.setText("Zoom")
2238 self.zoom_button.setDisabled(True)
2239 self.zoom_button.released.connect(lambda: self.Zoom())
2241 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2243 self.vbox = VBoxLayout(self.splitter, self.hbox)
2245 self.setLayout(self.vbox)
2247 def GetScaleForRangeX(self, xsubrange):
2248 # Default graph 1000 pixels wide
2249 dflt = 1000.0
2250 r = xsubrange.hi - xsubrange.lo
2251 return dflt / r
2253 def GetScaleForRangeY(self, ysubrange):
2254 # Default graph 50 pixels high
2255 dflt = 50.0
2256 r = ysubrange.hi - ysubrange.lo
2257 return dflt / r
2259 def GetScaleForRange(self, subrange):
2260 # Default graph 1000 pixels wide, 50 pixels high
2261 xscale = self.GetScaleForRangeX(subrange.x)
2262 yscale = self.GetScaleForRangeY(subrange.y)
2263 return XY(xscale, yscale)
2265 def PointEvent(self, cpu, time_from, time_to, hregions):
2266 text = "CPU: " + str(cpu)
2267 time_from = time_from.quantize(Decimal(1))
2268 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2269 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2270 self.point_label.setText(text)
2271 self.legend.Highlight(hregions)
2273 def RightClickEvent(self, cpu, hregion_times, pos):
2274 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2275 return
2276 menu = QMenu(self.view)
2277 for hregion, time in hregion_times:
2278 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2279 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2280 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2281 menu.exec_(pos)
2283 def RightClickSelect(self, args):
2284 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2286 def NoPointEvent(self):
2287 self.point_label.setText("")
2288 self.legend.Highlight({})
2290 def RangeEvent(self, time_from, time_to):
2291 time_from = time_from.quantize(Decimal(1))
2292 time_to = time_to.quantize(Decimal(1))
2293 if time_to <= time_from:
2294 self.point_label.setText("")
2295 return
2296 rel_time_from = time_from - self.start_time
2297 rel_time_to = time_to - self.start_time
2298 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2299 text = text + " duration: " + ToTimeStr(time_to - time_from)
2300 self.point_label.setText(text)
2302 def BackState(self):
2303 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2305 def PushBackState(self):
2306 state = copy.deepcopy(self.BackState())
2307 self.back_state.append(state)
2308 self.back_button.setEnabled(True)
2310 def PopBackState(self):
2311 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2312 self.attrs.Update()
2313 if not self.back_state:
2314 self.back_button.setDisabled(True)
2316 def PushForwardState(self):
2317 state = copy.deepcopy(self.BackState())
2318 self.forward_state.append(state)
2319 self.forward_button.setEnabled(True)
2321 def PopForwardState(self):
2322 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2323 self.attrs.Update()
2324 if not self.forward_state:
2325 self.forward_button.setDisabled(True)
2327 def Title(self):
2328 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2329 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2330 rel_time_from = time_from - self.start_time
2331 rel_time_to = time_to - self.start_time
2332 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2333 title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2334 return title
2336 def Update(self):
2337 selected_subrange, selection_state = self.selection_state
2338 self.item.SetSelection(selection_state)
2339 self.item.SetBracket(self.fwd_rect)
2340 self.zoom_button.setDisabled(selected_subrange is None)
2341 self.GraphTitleChanged(self.Title())
2342 self.item.update(self.item.boundingRect())
2344 def Back(self):
2345 if not self.back_state:
2346 return
2347 self.PushForwardState()
2348 self.PopBackState()
2349 self.Update()
2351 def Forward(self):
2352 if not self.forward_state:
2353 return
2354 self.PushBackState()
2355 self.PopForwardState()
2356 self.Update()
2358 def SelectEvent(self, x0, x1, selection_state):
2359 if selection_state is None:
2360 selected_subrange = None
2361 else:
2362 if x1 - x0 < 1.0:
2363 x1 += 1.0
2364 selected_subrange = Subrange(x0, x1)
2365 self.selection_state = (selected_subrange, selection_state)
2366 self.zoom_button.setDisabled(selected_subrange is None)
2368 def Zoom(self):
2369 selected_subrange, selection_state = self.selection_state
2370 if selected_subrange is None:
2371 return
2372 self.fwd_rect = selection_state
2373 self.item.SetSelection(None)
2374 self.PushBackState()
2375 self.attrs.subrange.x = selected_subrange
2376 self.forward_state = []
2377 self.forward_button.setDisabled(True)
2378 self.selection_state = (None, None)
2379 self.fwd_rect = None
2380 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2381 self.attrs.Update()
2382 self.Update()
2384 # Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2386 class SlowInitClass():
2388 def __init__(self, glb, title, init_fn):
2389 self.init_fn = init_fn
2390 self.done = False
2391 self.result = None
2393 self.msg_box = QMessageBox(glb.mainwindow)
2394 self.msg_box.setText("Initializing " + title + ". Please wait.")
2395 self.msg_box.setWindowTitle("Initializing " + title)
2396 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2398 self.init_thread = Thread(self.ThreadFn, glb)
2399 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2401 self.init_thread.start()
2403 def Done(self):
2404 self.msg_box.done(0)
2406 def ThreadFn(self, glb):
2407 conn_name = "SlowInitClass" + str(os.getpid())
2408 db, dbname = glb.dbref.Open(conn_name)
2409 self.result = self.init_fn(db)
2410 self.done = True
2411 return (True, 0)
2413 def Result(self):
2414 while not self.done:
2415 self.msg_box.exec_()
2416 self.init_thread.wait()
2417 return self.result
2419 def SlowInit(glb, title, init_fn):
2420 init = SlowInitClass(glb, title, init_fn)
2421 return init.Result()
2423 # Time chart by CPU window
2425 class TimeChartByCPUWindow(QMdiSubWindow):
2427 def __init__(self, glb, parent=None):
2428 super(TimeChartByCPUWindow, self).__init__(parent)
2430 self.glb = glb
2431 self.machine_id = glb.HostMachineId()
2432 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2434 collection = LookupModel(self.collection_name)
2435 if collection is None:
2436 collection = SlowInit(glb, "Time Chart", self.Init)
2438 self.widget = SwitchGraphWidget(glb, collection, self)
2439 self.view = self.widget
2441 self.base_title = "Time Chart by CPU"
2442 self.setWindowTitle(self.base_title + self.widget.Title())
2443 self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2445 self.setWidget(self.widget)
2447 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2449 def Init(self, db):
2450 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2452 def GraphTitleChanged(self, title):
2453 self.setWindowTitle(self.base_title + " : " + title)
2455 # Child data item finder
2457 class ChildDataItemFinder():
2459 def __init__(self, root):
2460 self.root = root
2461 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2462 self.rows = []
2463 self.pos = 0
2465 def FindSelect(self):
2466 self.rows = []
2467 if self.pattern:
2468 pattern = re.compile(self.value)
2469 for child in self.root.child_items:
2470 for column_data in child.data:
2471 if re.search(pattern, str(column_data)) is not None:
2472 self.rows.append(child.row)
2473 break
2474 else:
2475 for child in self.root.child_items:
2476 for column_data in child.data:
2477 if self.value in str(column_data):
2478 self.rows.append(child.row)
2479 break
2481 def FindValue(self):
2482 self.pos = 0
2483 if self.last_value != self.value or self.pattern != self.last_pattern:
2484 self.FindSelect()
2485 if not len(self.rows):
2486 return -1
2487 return self.rows[self.pos]
2489 def FindThread(self):
2490 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2491 row = self.FindValue()
2492 elif len(self.rows):
2493 if self.direction > 0:
2494 self.pos += 1
2495 if self.pos >= len(self.rows):
2496 self.pos = 0
2497 else:
2498 self.pos -= 1
2499 if self.pos < 0:
2500 self.pos = len(self.rows) - 1
2501 row = self.rows[self.pos]
2502 else:
2503 row = -1
2504 return (True, row)
2506 def Find(self, value, direction, pattern, context, callback):
2507 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2508 # Use a thread so the UI is not blocked
2509 thread = Thread(self.FindThread)
2510 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2511 thread.start()
2513 def FindDone(self, thread, callback, row):
2514 callback(row)
2516 # Number of database records to fetch in one go
2518 glb_chunk_sz = 10000
2520 # Background process for SQL data fetcher
2522 class SQLFetcherProcess():
2524 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2525 # Need a unique connection name
2526 conn_name = "SQLFetcher" + str(os.getpid())
2527 self.db, dbname = dbref.Open(conn_name)
2528 self.sql = sql
2529 self.buffer = buffer
2530 self.head = head
2531 self.tail = tail
2532 self.fetch_count = fetch_count
2533 self.fetching_done = fetching_done
2534 self.process_target = process_target
2535 self.wait_event = wait_event
2536 self.fetched_event = fetched_event
2537 self.prep = prep
2538 self.query = QSqlQuery(self.db)
2539 self.query_limit = 0 if "$$last_id$$" in sql else 2
2540 self.last_id = -1
2541 self.fetched = 0
2542 self.more = True
2543 self.local_head = self.head.value
2544 self.local_tail = self.tail.value
2546 def Select(self):
2547 if self.query_limit:
2548 if self.query_limit == 1:
2549 return
2550 self.query_limit -= 1
2551 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2552 QueryExec(self.query, stmt)
2554 def Next(self):
2555 if not self.query.next():
2556 self.Select()
2557 if not self.query.next():
2558 return None
2559 self.last_id = self.query.value(0)
2560 return self.prep(self.query)
2562 def WaitForTarget(self):
2563 while True:
2564 self.wait_event.clear()
2565 target = self.process_target.value
2566 if target > self.fetched or target < 0:
2567 break
2568 self.wait_event.wait()
2569 return target
2571 def HasSpace(self, sz):
2572 if self.local_tail <= self.local_head:
2573 space = len(self.buffer) - self.local_head
2574 if space > sz:
2575 return True
2576 if space >= glb_nsz:
2577 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2578 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2579 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2580 self.local_head = 0
2581 if self.local_tail - self.local_head > sz:
2582 return True
2583 return False
2585 def WaitForSpace(self, sz):
2586 if self.HasSpace(sz):
2587 return
2588 while True:
2589 self.wait_event.clear()
2590 self.local_tail = self.tail.value
2591 if self.HasSpace(sz):
2592 return
2593 self.wait_event.wait()
2595 def AddToBuffer(self, obj):
2596 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2597 n = len(d)
2598 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2599 sz = n + glb_nsz
2600 self.WaitForSpace(sz)
2601 pos = self.local_head
2602 self.buffer[pos : pos + len(nd)] = nd
2603 self.buffer[pos + glb_nsz : pos + sz] = d
2604 self.local_head += sz
2606 def FetchBatch(self, batch_size):
2607 fetched = 0
2608 while batch_size > fetched:
2609 obj = self.Next()
2610 if obj is None:
2611 self.more = False
2612 break
2613 self.AddToBuffer(obj)
2614 fetched += 1
2615 if fetched:
2616 self.fetched += fetched
2617 with self.fetch_count.get_lock():
2618 self.fetch_count.value += fetched
2619 self.head.value = self.local_head
2620 self.fetched_event.set()
2622 def Run(self):
2623 while self.more:
2624 target = self.WaitForTarget()
2625 if target < 0:
2626 break
2627 batch_size = min(glb_chunk_sz, target - self.fetched)
2628 self.FetchBatch(batch_size)
2629 self.fetching_done.value = True
2630 self.fetched_event.set()
2632 def SQLFetcherFn(*x):
2633 process = SQLFetcherProcess(*x)
2634 process.Run()
2636 # SQL data fetcher
2638 class SQLFetcher(QObject):
2640 done = Signal(object)
2642 def __init__(self, glb, sql, prep, process_data, parent=None):
2643 super(SQLFetcher, self).__init__(parent)
2644 self.process_data = process_data
2645 self.more = True
2646 self.target = 0
2647 self.last_target = 0
2648 self.fetched = 0
2649 self.buffer_size = 16 * 1024 * 1024
2650 self.buffer = Array(c_char, self.buffer_size, lock=False)
2651 self.head = Value(c_longlong)
2652 self.tail = Value(c_longlong)
2653 self.local_tail = 0
2654 self.fetch_count = Value(c_longlong)
2655 self.fetching_done = Value(c_bool)
2656 self.last_count = 0
2657 self.process_target = Value(c_longlong)
2658 self.wait_event = Event()
2659 self.fetched_event = Event()
2660 glb.AddInstanceToShutdownOnExit(self)
2661 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))
2662 self.process.start()
2663 self.thread = Thread(self.Thread)
2664 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2665 self.thread.start()
2667 def Shutdown(self):
2668 # Tell the thread and process to exit
2669 self.process_target.value = -1
2670 self.wait_event.set()
2671 self.more = False
2672 self.fetching_done.value = True
2673 self.fetched_event.set()
2675 def Thread(self):
2676 if not self.more:
2677 return True, 0
2678 while True:
2679 self.fetched_event.clear()
2680 fetch_count = self.fetch_count.value
2681 if fetch_count != self.last_count:
2682 break
2683 if self.fetching_done.value:
2684 self.more = False
2685 return True, 0
2686 self.fetched_event.wait()
2687 count = fetch_count - self.last_count
2688 self.last_count = fetch_count
2689 self.fetched += count
2690 return False, count
2692 def Fetch(self, nr):
2693 if not self.more:
2694 # -1 inidcates there are no more
2695 return -1
2696 result = self.fetched
2697 extra = result + nr - self.target
2698 if extra > 0:
2699 self.target += extra
2700 # process_target < 0 indicates shutting down
2701 if self.process_target.value >= 0:
2702 self.process_target.value = self.target
2703 self.wait_event.set()
2704 return result
2706 def RemoveFromBuffer(self):
2707 pos = self.local_tail
2708 if len(self.buffer) - pos < glb_nsz:
2709 pos = 0
2710 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2711 if n == 0:
2712 pos = 0
2713 n = pickle.loads(self.buffer[0 : glb_nsz])
2714 pos += glb_nsz
2715 obj = pickle.loads(self.buffer[pos : pos + n])
2716 self.local_tail = pos + n
2717 return obj
2719 def ProcessData(self, count):
2720 for i in xrange(count):
2721 obj = self.RemoveFromBuffer()
2722 self.process_data(obj)
2723 self.tail.value = self.local_tail
2724 self.wait_event.set()
2725 self.done.emit(count)
2727 # Fetch more records bar
2729 class FetchMoreRecordsBar():
2731 def __init__(self, model, parent):
2732 self.model = model
2734 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2735 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2737 self.fetch_count = QSpinBox()
2738 self.fetch_count.setRange(1, 1000000)
2739 self.fetch_count.setValue(10)
2740 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2742 self.fetch = QPushButton("Go!")
2743 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2744 self.fetch.released.connect(self.FetchMoreRecords)
2746 self.progress = QProgressBar()
2747 self.progress.setRange(0, 100)
2748 self.progress.hide()
2750 self.done_label = QLabel("All records fetched")
2751 self.done_label.hide()
2753 self.spacer = QLabel("")
2755 self.close_button = QToolButton()
2756 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2757 self.close_button.released.connect(self.Deactivate)
2759 self.hbox = QHBoxLayout()
2760 self.hbox.setContentsMargins(0, 0, 0, 0)
2762 self.hbox.addWidget(self.label)
2763 self.hbox.addWidget(self.fetch_count)
2764 self.hbox.addWidget(self.fetch)
2765 self.hbox.addWidget(self.spacer)
2766 self.hbox.addWidget(self.progress)
2767 self.hbox.addWidget(self.done_label)
2768 self.hbox.addWidget(self.close_button)
2770 self.bar = QWidget()
2771 self.bar.setLayout(self.hbox)
2772 self.bar.show()
2774 self.in_progress = False
2775 self.model.progress.connect(self.Progress)
2777 self.done = False
2779 if not model.HasMoreRecords():
2780 self.Done()
2782 def Widget(self):
2783 return self.bar
2785 def Activate(self):
2786 self.bar.show()
2787 self.fetch.setFocus()
2789 def Deactivate(self):
2790 self.bar.hide()
2792 def Enable(self, enable):
2793 self.fetch.setEnabled(enable)
2794 self.fetch_count.setEnabled(enable)
2796 def Busy(self):
2797 self.Enable(False)
2798 self.fetch.hide()
2799 self.spacer.hide()
2800 self.progress.show()
2802 def Idle(self):
2803 self.in_progress = False
2804 self.Enable(True)
2805 self.progress.hide()
2806 self.fetch.show()
2807 self.spacer.show()
2809 def Target(self):
2810 return self.fetch_count.value() * glb_chunk_sz
2812 def Done(self):
2813 self.done = True
2814 self.Idle()
2815 self.label.hide()
2816 self.fetch_count.hide()
2817 self.fetch.hide()
2818 self.spacer.hide()
2819 self.done_label.show()
2821 def Progress(self, count):
2822 if self.in_progress:
2823 if count:
2824 percent = ((count - self.start) * 100) / self.Target()
2825 if percent >= 100:
2826 self.Idle()
2827 else:
2828 self.progress.setValue(percent)
2829 if not count:
2830 # Count value of zero means no more records
2831 self.Done()
2833 def FetchMoreRecords(self):
2834 if self.done:
2835 return
2836 self.progress.setValue(0)
2837 self.Busy()
2838 self.in_progress = True
2839 self.start = self.model.FetchMoreRecords(self.Target())
2841 # Brance data model level two item
2843 class BranchLevelTwoItem():
2845 def __init__(self, row, col, text, parent_item):
2846 self.row = row
2847 self.parent_item = parent_item
2848 self.data = [""] * (col + 1)
2849 self.data[col] = text
2850 self.level = 2
2852 def getParentItem(self):
2853 return self.parent_item
2855 def getRow(self):
2856 return self.row
2858 def childCount(self):
2859 return 0
2861 def hasChildren(self):
2862 return False
2864 def getData(self, column):
2865 return self.data[column]
2867 # Brance data model level one item
2869 class BranchLevelOneItem():
2871 def __init__(self, glb, row, data, parent_item):
2872 self.glb = glb
2873 self.row = row
2874 self.parent_item = parent_item
2875 self.child_count = 0
2876 self.child_items = []
2877 self.data = data[1:]
2878 self.dbid = data[0]
2879 self.level = 1
2880 self.query_done = False
2881 self.br_col = len(self.data) - 1
2883 def getChildItem(self, row):
2884 return self.child_items[row]
2886 def getParentItem(self):
2887 return self.parent_item
2889 def getRow(self):
2890 return self.row
2892 def Select(self):
2893 self.query_done = True
2895 if not self.glb.have_disassembler:
2896 return
2898 query = QSqlQuery(self.glb.db)
2900 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2901 " FROM samples"
2902 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2903 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2904 " WHERE samples.id = " + str(self.dbid))
2905 if not query.next():
2906 return
2907 cpu = query.value(0)
2908 dso = query.value(1)
2909 sym = query.value(2)
2910 if dso == 0 or sym == 0:
2911 return
2912 off = query.value(3)
2913 short_name = query.value(4)
2914 long_name = query.value(5)
2915 build_id = query.value(6)
2916 sym_start = query.value(7)
2917 ip = query.value(8)
2919 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2920 " FROM samples"
2921 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2922 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2923 " ORDER BY samples.id"
2924 " LIMIT 1")
2925 if not query.next():
2926 return
2927 if query.value(0) != dso:
2928 # Cannot disassemble from one dso to another
2929 return
2930 bsym = query.value(1)
2931 boff = query.value(2)
2932 bsym_start = query.value(3)
2933 if bsym == 0:
2934 return
2935 tot = bsym_start + boff + 1 - sym_start - off
2936 if tot <= 0 or tot > 16384:
2937 return
2939 inst = self.glb.disassembler.Instruction()
2940 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2941 if not f:
2942 return
2943 mode = 0 if Is64Bit(f) else 1
2944 self.glb.disassembler.SetMode(inst, mode)
2946 buf_sz = tot + 16
2947 buf = create_string_buffer(tot + 16)
2948 f.seek(sym_start + off)
2949 buf.value = f.read(buf_sz)
2950 buf_ptr = addressof(buf)
2951 i = 0
2952 while tot > 0:
2953 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2954 if cnt:
2955 byte_str = tohex(ip).rjust(16)
2956 for k in xrange(cnt):
2957 byte_str += " %02x" % ord(buf[i])
2958 i += 1
2959 while k < 15:
2960 byte_str += " "
2961 k += 1
2962 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2963 self.child_count += 1
2964 else:
2965 return
2966 buf_ptr += cnt
2967 tot -= cnt
2968 buf_sz -= cnt
2969 ip += cnt
2971 def childCount(self):
2972 if not self.query_done:
2973 self.Select()
2974 if not self.child_count:
2975 return -1
2976 return self.child_count
2978 def hasChildren(self):
2979 if not self.query_done:
2980 return True
2981 return self.child_count > 0
2983 def getData(self, column):
2984 return self.data[column]
2986 # Brance data model root item
2988 class BranchRootItem():
2990 def __init__(self):
2991 self.child_count = 0
2992 self.child_items = []
2993 self.level = 0
2995 def getChildItem(self, row):
2996 return self.child_items[row]
2998 def getParentItem(self):
2999 return None
3001 def getRow(self):
3002 return 0
3004 def childCount(self):
3005 return self.child_count
3007 def hasChildren(self):
3008 return self.child_count > 0
3010 def getData(self, column):
3011 return ""
3013 # Calculate instructions per cycle
3015 def CalcIPC(cyc_cnt, insn_cnt):
3016 if cyc_cnt and insn_cnt:
3017 ipc = Decimal(float(insn_cnt) / cyc_cnt)
3018 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3019 else:
3020 ipc = "0"
3021 return ipc
3023 # Branch data preparation
3025 def BranchDataPrepBr(query, data):
3026 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3027 " (" + dsoname(query.value(11)) + ")" + " -> " +
3028 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3029 " (" + dsoname(query.value(15)) + ")")
3031 def BranchDataPrepIPC(query, data):
3032 insn_cnt = query.value(16)
3033 cyc_cnt = query.value(17)
3034 ipc = CalcIPC(cyc_cnt, insn_cnt)
3035 data.append(insn_cnt)
3036 data.append(cyc_cnt)
3037 data.append(ipc)
3039 def BranchDataPrep(query):
3040 data = []
3041 for i in xrange(0, 8):
3042 data.append(query.value(i))
3043 BranchDataPrepBr(query, data)
3044 return data
3046 def BranchDataPrepWA(query):
3047 data = []
3048 data.append(query.value(0))
3049 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3050 data.append("{:>19}".format(query.value(1)))
3051 for i in xrange(2, 8):
3052 data.append(query.value(i))
3053 BranchDataPrepBr(query, data)
3054 return data
3056 def BranchDataWithIPCPrep(query):
3057 data = []
3058 for i in xrange(0, 8):
3059 data.append(query.value(i))
3060 BranchDataPrepIPC(query, data)
3061 BranchDataPrepBr(query, data)
3062 return data
3064 def BranchDataWithIPCPrepWA(query):
3065 data = []
3066 data.append(query.value(0))
3067 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3068 data.append("{:>19}".format(query.value(1)))
3069 for i in xrange(2, 8):
3070 data.append(query.value(i))
3071 BranchDataPrepIPC(query, data)
3072 BranchDataPrepBr(query, data)
3073 return data
3075 # Branch data model
3077 class BranchModel(TreeModel):
3079 progress = Signal(object)
3081 def __init__(self, glb, event_id, where_clause, parent=None):
3082 super(BranchModel, self).__init__(glb, None, parent)
3083 self.event_id = event_id
3084 self.more = True
3085 self.populated = 0
3086 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3087 if self.have_ipc:
3088 select_ipc = ", insn_count, cyc_count"
3089 prep_fn = BranchDataWithIPCPrep
3090 prep_wa_fn = BranchDataWithIPCPrepWA
3091 else:
3092 select_ipc = ""
3093 prep_fn = BranchDataPrep
3094 prep_wa_fn = BranchDataPrepWA
3095 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3096 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3097 " ip, symbols.name, sym_offset, dsos.short_name,"
3098 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3099 + select_ipc +
3100 " FROM samples"
3101 " INNER JOIN comms ON comm_id = comms.id"
3102 " INNER JOIN threads ON thread_id = threads.id"
3103 " INNER JOIN branch_types ON branch_type = branch_types.id"
3104 " INNER JOIN symbols ON symbol_id = symbols.id"
3105 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3106 " INNER JOIN dsos ON samples.dso_id = dsos.id"
3107 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3108 " WHERE samples.id > $$last_id$$" + where_clause +
3109 " AND evsel_id = " + str(self.event_id) +
3110 " ORDER BY samples.id"
3111 " LIMIT " + str(glb_chunk_sz))
3112 if pyside_version_1 and sys.version_info[0] == 3:
3113 prep = prep_fn
3114 else:
3115 prep = prep_wa_fn
3116 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3117 self.fetcher.done.connect(self.Update)
3118 self.fetcher.Fetch(glb_chunk_sz)
3120 def GetRoot(self):
3121 return BranchRootItem()
3123 def columnCount(self, parent=None):
3124 if self.have_ipc:
3125 return 11
3126 else:
3127 return 8
3129 def columnHeader(self, column):
3130 if self.have_ipc:
3131 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3132 else:
3133 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3135 def columnFont(self, column):
3136 if self.have_ipc:
3137 br_col = 10
3138 else:
3139 br_col = 7
3140 if column != br_col:
3141 return None
3142 return QFont("Monospace")
3144 def DisplayData(self, item, index):
3145 if item.level == 1:
3146 self.FetchIfNeeded(item.row)
3147 return item.getData(index.column())
3149 def AddSample(self, data):
3150 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3151 self.root.child_items.append(child)
3152 self.populated += 1
3154 def Update(self, fetched):
3155 if not fetched:
3156 self.more = False
3157 self.progress.emit(0)
3158 child_count = self.root.child_count
3159 count = self.populated - child_count
3160 if count > 0:
3161 parent = QModelIndex()
3162 self.beginInsertRows(parent, child_count, child_count + count - 1)
3163 self.insertRows(child_count, count, parent)
3164 self.root.child_count += count
3165 self.endInsertRows()
3166 self.progress.emit(self.root.child_count)
3168 def FetchMoreRecords(self, count):
3169 current = self.root.child_count
3170 if self.more:
3171 self.fetcher.Fetch(count)
3172 else:
3173 self.progress.emit(0)
3174 return current
3176 def HasMoreRecords(self):
3177 return self.more
3179 # Report Variables
3181 class ReportVars():
3183 def __init__(self, name = "", where_clause = "", limit = ""):
3184 self.name = name
3185 self.where_clause = where_clause
3186 self.limit = limit
3188 def UniqueId(self):
3189 return str(self.where_clause + ";" + self.limit)
3191 # Branch window
3193 class BranchWindow(QMdiSubWindow):
3195 def __init__(self, glb, event_id, report_vars, parent=None):
3196 super(BranchWindow, self).__init__(parent)
3198 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
3200 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3202 self.view = QTreeView()
3203 self.view.setUniformRowHeights(True)
3204 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3205 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3206 self.view.setModel(self.model)
3208 self.ResizeColumnsToContents()
3210 self.context_menu = TreeContextMenu(self.view)
3212 self.find_bar = FindBar(self, self, True)
3214 self.finder = ChildDataItemFinder(self.model.root)
3216 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3218 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3220 self.setWidget(self.vbox.Widget())
3222 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3224 def ResizeColumnToContents(self, column, n):
3225 # Using the view's resizeColumnToContents() here is extrememly slow
3226 # so implement a crude alternative
3227 mm = "MM" if column else "MMMM"
3228 font = self.view.font()
3229 metrics = QFontMetrics(font)
3230 max = 0
3231 for row in xrange(n):
3232 val = self.model.root.child_items[row].data[column]
3233 len = metrics.width(str(val) + mm)
3234 max = len if len > max else max
3235 val = self.model.columnHeader(column)
3236 len = metrics.width(str(val) + mm)
3237 max = len if len > max else max
3238 self.view.setColumnWidth(column, max)
3240 def ResizeColumnsToContents(self):
3241 n = min(self.model.root.child_count, 100)
3242 if n < 1:
3243 # No data yet, so connect a signal to notify when there is
3244 self.model.rowsInserted.connect(self.UpdateColumnWidths)
3245 return
3246 columns = self.model.columnCount()
3247 for i in xrange(columns):
3248 self.ResizeColumnToContents(i, n)
3250 def UpdateColumnWidths(self, *x):
3251 # This only needs to be done once, so disconnect the signal now
3252 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3253 self.ResizeColumnsToContents()
3255 def Find(self, value, direction, pattern, context):
3256 self.view.setFocus()
3257 self.find_bar.Busy()
3258 self.finder.Find(value, direction, pattern, context, self.FindDone)
3260 def FindDone(self, row):
3261 self.find_bar.Idle()
3262 if row >= 0:
3263 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3264 else:
3265 self.find_bar.NotFound()
3267 # Line edit data item
3269 class LineEditDataItem(object):
3271 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3272 self.glb = glb
3273 self.label = label
3274 self.placeholder_text = placeholder_text
3275 self.parent = parent
3276 self.id = id
3278 self.value = default
3280 self.widget = QLineEdit(default)
3281 self.widget.editingFinished.connect(self.Validate)
3282 self.widget.textChanged.connect(self.Invalidate)
3283 self.red = False
3284 self.error = ""
3285 self.validated = True
3287 if placeholder_text:
3288 self.widget.setPlaceholderText(placeholder_text)
3290 def TurnTextRed(self):
3291 if not self.red:
3292 palette = QPalette()
3293 palette.setColor(QPalette.Text,Qt.red)
3294 self.widget.setPalette(palette)
3295 self.red = True
3297 def TurnTextNormal(self):
3298 if self.red:
3299 palette = QPalette()
3300 self.widget.setPalette(palette)
3301 self.red = False
3303 def InvalidValue(self, value):
3304 self.value = ""
3305 self.TurnTextRed()
3306 self.error = self.label + " invalid value '" + value + "'"
3307 self.parent.ShowMessage(self.error)
3309 def Invalidate(self):
3310 self.validated = False
3312 def DoValidate(self, input_string):
3313 self.value = input_string.strip()
3315 def Validate(self):
3316 self.validated = True
3317 self.error = ""
3318 self.TurnTextNormal()
3319 self.parent.ClearMessage()
3320 input_string = self.widget.text()
3321 if not len(input_string.strip()):
3322 self.value = ""
3323 return
3324 self.DoValidate(input_string)
3326 def IsValid(self):
3327 if not self.validated:
3328 self.Validate()
3329 if len(self.error):
3330 self.parent.ShowMessage(self.error)
3331 return False
3332 return True
3334 def IsNumber(self, value):
3335 try:
3336 x = int(value)
3337 except:
3338 x = 0
3339 return str(x) == value
3341 # Non-negative integer ranges dialog data item
3343 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3345 def __init__(self, glb, label, placeholder_text, column_name, parent):
3346 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3348 self.column_name = column_name
3350 def DoValidate(self, input_string):
3351 singles = []
3352 ranges = []
3353 for value in [x.strip() for x in input_string.split(",")]:
3354 if "-" in value:
3355 vrange = value.split("-")
3356 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3357 return self.InvalidValue(value)
3358 ranges.append(vrange)
3359 else:
3360 if not self.IsNumber(value):
3361 return self.InvalidValue(value)
3362 singles.append(value)
3363 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3364 if len(singles):
3365 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3366 self.value = " OR ".join(ranges)
3368 # Positive integer dialog data item
3370 class PositiveIntegerDataItem(LineEditDataItem):
3372 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3373 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3375 def DoValidate(self, input_string):
3376 if not self.IsNumber(input_string.strip()):
3377 return self.InvalidValue(input_string)
3378 value = int(input_string.strip())
3379 if value <= 0:
3380 return self.InvalidValue(input_string)
3381 self.value = str(value)
3383 # Dialog data item converted and validated using a SQL table
3385 class SQLTableDataItem(LineEditDataItem):
3387 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3388 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3390 self.table_name = table_name
3391 self.match_column = match_column
3392 self.column_name1 = column_name1
3393 self.column_name2 = column_name2
3395 def ValueToIds(self, value):
3396 ids = []
3397 query = QSqlQuery(self.glb.db)
3398 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3399 ret = query.exec_(stmt)
3400 if ret:
3401 while query.next():
3402 ids.append(str(query.value(0)))
3403 return ids
3405 def DoValidate(self, input_string):
3406 all_ids = []
3407 for value in [x.strip() for x in input_string.split(",")]:
3408 ids = self.ValueToIds(value)
3409 if len(ids):
3410 all_ids.extend(ids)
3411 else:
3412 return self.InvalidValue(value)
3413 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3414 if self.column_name2:
3415 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3417 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
3419 class SampleTimeRangesDataItem(LineEditDataItem):
3421 def __init__(self, glb, label, placeholder_text, column_name, parent):
3422 self.column_name = column_name
3424 self.last_id = 0
3425 self.first_time = 0
3426 self.last_time = 2 ** 64
3428 query = QSqlQuery(glb.db)
3429 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3430 if query.next():
3431 self.last_id = int(query.value(0))
3432 self.first_time = int(glb.HostStartTime())
3433 self.last_time = int(glb.HostFinishTime())
3434 if placeholder_text:
3435 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3437 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3439 def IdBetween(self, query, lower_id, higher_id, order):
3440 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3441 if query.next():
3442 return True, int(query.value(0))
3443 else:
3444 return False, 0
3446 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3447 query = QSqlQuery(self.glb.db)
3448 while True:
3449 next_id = int((lower_id + higher_id) / 2)
3450 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3451 if not query.next():
3452 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3453 if not ok:
3454 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3455 if not ok:
3456 return str(higher_id)
3457 next_id = dbid
3458 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3459 next_time = int(query.value(0))
3460 if get_floor:
3461 if target_time > next_time:
3462 lower_id = next_id
3463 else:
3464 higher_id = next_id
3465 if higher_id <= lower_id + 1:
3466 return str(higher_id)
3467 else:
3468 if target_time >= next_time:
3469 lower_id = next_id
3470 else:
3471 higher_id = next_id
3472 if higher_id <= lower_id + 1:
3473 return str(lower_id)
3475 def ConvertRelativeTime(self, val):
3476 mult = 1
3477 suffix = val[-2:]
3478 if suffix == "ms":
3479 mult = 1000000
3480 elif suffix == "us":
3481 mult = 1000
3482 elif suffix == "ns":
3483 mult = 1
3484 else:
3485 return val
3486 val = val[:-2].strip()
3487 if not self.IsNumber(val):
3488 return val
3489 val = int(val) * mult
3490 if val >= 0:
3491 val += self.first_time
3492 else:
3493 val += self.last_time
3494 return str(val)
3496 def ConvertTimeRange(self, vrange):
3497 if vrange[0] == "":
3498 vrange[0] = str(self.first_time)
3499 if vrange[1] == "":
3500 vrange[1] = str(self.last_time)
3501 vrange[0] = self.ConvertRelativeTime(vrange[0])
3502 vrange[1] = self.ConvertRelativeTime(vrange[1])
3503 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3504 return False
3505 beg_range = max(int(vrange[0]), self.first_time)
3506 end_range = min(int(vrange[1]), self.last_time)
3507 if beg_range > self.last_time or end_range < self.first_time:
3508 return False
3509 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3510 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3511 return True
3513 def AddTimeRange(self, value, ranges):
3514 n = value.count("-")
3515 if n == 1:
3516 pass
3517 elif n == 2:
3518 if value.split("-")[1].strip() == "":
3519 n = 1
3520 elif n == 3:
3521 n = 2
3522 else:
3523 return False
3524 pos = findnth(value, "-", n)
3525 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3526 if self.ConvertTimeRange(vrange):
3527 ranges.append(vrange)
3528 return True
3529 return False
3531 def DoValidate(self, input_string):
3532 ranges = []
3533 for value in [x.strip() for x in input_string.split(",")]:
3534 if not self.AddTimeRange(value, ranges):
3535 return self.InvalidValue(value)
3536 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3537 self.value = " OR ".join(ranges)
3539 # Report Dialog Base
3541 class ReportDialogBase(QDialog):
3543 def __init__(self, glb, title, items, partial, parent=None):
3544 super(ReportDialogBase, self).__init__(parent)
3546 self.glb = glb
3548 self.report_vars = ReportVars()
3550 self.setWindowTitle(title)
3551 self.setMinimumWidth(600)
3553 self.data_items = [x(glb, self) for x in items]
3555 self.partial = partial
3557 self.grid = QGridLayout()
3559 for row in xrange(len(self.data_items)):
3560 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3561 self.grid.addWidget(self.data_items[row].widget, row, 1)
3563 self.status = QLabel()
3565 self.ok_button = QPushButton("Ok", self)
3566 self.ok_button.setDefault(True)
3567 self.ok_button.released.connect(self.Ok)
3568 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3570 self.cancel_button = QPushButton("Cancel", self)
3571 self.cancel_button.released.connect(self.reject)
3572 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3574 self.hbox = QHBoxLayout()
3575 #self.hbox.addStretch()
3576 self.hbox.addWidget(self.status)
3577 self.hbox.addWidget(self.ok_button)
3578 self.hbox.addWidget(self.cancel_button)
3580 self.vbox = QVBoxLayout()
3581 self.vbox.addLayout(self.grid)
3582 self.vbox.addLayout(self.hbox)
3584 self.setLayout(self.vbox)
3586 def Ok(self):
3587 vars = self.report_vars
3588 for d in self.data_items:
3589 if d.id == "REPORTNAME":
3590 vars.name = d.value
3591 if not vars.name:
3592 self.ShowMessage("Report name is required")
3593 return
3594 for d in self.data_items:
3595 if not d.IsValid():
3596 return
3597 for d in self.data_items[1:]:
3598 if d.id == "LIMIT":
3599 vars.limit = d.value
3600 elif len(d.value):
3601 if len(vars.where_clause):
3602 vars.where_clause += " AND "
3603 vars.where_clause += d.value
3604 if len(vars.where_clause):
3605 if self.partial:
3606 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3607 else:
3608 vars.where_clause = " WHERE " + vars.where_clause + " "
3609 self.accept()
3611 def ShowMessage(self, msg):
3612 self.status.setText("<font color=#FF0000>" + msg)
3614 def ClearMessage(self):
3615 self.status.setText("")
3617 # Selected branch report creation dialog
3619 class SelectedBranchDialog(ReportDialogBase):
3621 def __init__(self, glb, parent=None):
3622 title = "Selected Branches"
3623 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3624 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3625 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3626 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3627 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3628 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3629 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3630 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3631 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3632 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3634 # Event list
3636 def GetEventList(db):
3637 events = []
3638 query = QSqlQuery(db)
3639 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3640 while query.next():
3641 events.append(query.value(0))
3642 return events
3644 # Is a table selectable
3646 def IsSelectable(db, table, sql = "", columns = "*"):
3647 query = QSqlQuery(db)
3648 try:
3649 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3650 except:
3651 return False
3652 return True
3654 # SQL table data model item
3656 class SQLTableItem():
3658 def __init__(self, row, data):
3659 self.row = row
3660 self.data = data
3662 def getData(self, column):
3663 return self.data[column]
3665 # SQL table data model
3667 class SQLTableModel(TableModel):
3669 progress = Signal(object)
3671 def __init__(self, glb, sql, column_headers, parent=None):
3672 super(SQLTableModel, self).__init__(parent)
3673 self.glb = glb
3674 self.more = True
3675 self.populated = 0
3676 self.column_headers = column_headers
3677 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3678 self.fetcher.done.connect(self.Update)
3679 self.fetcher.Fetch(glb_chunk_sz)
3681 def DisplayData(self, item, index):
3682 self.FetchIfNeeded(item.row)
3683 return item.getData(index.column())
3685 def AddSample(self, data):
3686 child = SQLTableItem(self.populated, data)
3687 self.child_items.append(child)
3688 self.populated += 1
3690 def Update(self, fetched):
3691 if not fetched:
3692 self.more = False
3693 self.progress.emit(0)
3694 child_count = self.child_count
3695 count = self.populated - child_count
3696 if count > 0:
3697 parent = QModelIndex()
3698 self.beginInsertRows(parent, child_count, child_count + count - 1)
3699 self.insertRows(child_count, count, parent)
3700 self.child_count += count
3701 self.endInsertRows()
3702 self.progress.emit(self.child_count)
3704 def FetchMoreRecords(self, count):
3705 current = self.child_count
3706 if self.more:
3707 self.fetcher.Fetch(count)
3708 else:
3709 self.progress.emit(0)
3710 return current
3712 def HasMoreRecords(self):
3713 return self.more
3715 def columnCount(self, parent=None):
3716 return len(self.column_headers)
3718 def columnHeader(self, column):
3719 return self.column_headers[column]
3721 def SQLTableDataPrep(self, query, count):
3722 data = []
3723 for i in xrange(count):
3724 data.append(query.value(i))
3725 return data
3727 # SQL automatic table data model
3729 class SQLAutoTableModel(SQLTableModel):
3731 def __init__(self, glb, table_name, parent=None):
3732 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3733 if table_name == "comm_threads_view":
3734 # For now, comm_threads_view has no id column
3735 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3736 column_headers = []
3737 query = QSqlQuery(glb.db)
3738 if glb.dbref.is_sqlite3:
3739 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3740 while query.next():
3741 column_headers.append(query.value(1))
3742 if table_name == "sqlite_master":
3743 sql = "SELECT * FROM " + table_name
3744 else:
3745 if table_name[:19] == "information_schema.":
3746 sql = "SELECT * FROM " + table_name
3747 select_table_name = table_name[19:]
3748 schema = "information_schema"
3749 else:
3750 select_table_name = table_name
3751 schema = "public"
3752 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3753 while query.next():
3754 column_headers.append(query.value(0))
3755 if pyside_version_1 and sys.version_info[0] == 3:
3756 if table_name == "samples_view":
3757 self.SQLTableDataPrep = self.samples_view_DataPrep
3758 if table_name == "samples":
3759 self.SQLTableDataPrep = self.samples_DataPrep
3760 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3762 def samples_view_DataPrep(self, query, count):
3763 data = []
3764 data.append(query.value(0))
3765 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3766 data.append("{:>19}".format(query.value(1)))
3767 for i in xrange(2, count):
3768 data.append(query.value(i))
3769 return data
3771 def samples_DataPrep(self, query, count):
3772 data = []
3773 for i in xrange(9):
3774 data.append(query.value(i))
3775 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3776 data.append("{:>19}".format(query.value(9)))
3777 for i in xrange(10, count):
3778 data.append(query.value(i))
3779 return data
3781 # Base class for custom ResizeColumnsToContents
3783 class ResizeColumnsToContentsBase(QObject):
3785 def __init__(self, parent=None):
3786 super(ResizeColumnsToContentsBase, self).__init__(parent)
3788 def ResizeColumnToContents(self, column, n):
3789 # Using the view's resizeColumnToContents() here is extrememly slow
3790 # so implement a crude alternative
3791 font = self.view.font()
3792 metrics = QFontMetrics(font)
3793 max = 0
3794 for row in xrange(n):
3795 val = self.data_model.child_items[row].data[column]
3796 len = metrics.width(str(val) + "MM")
3797 max = len if len > max else max
3798 val = self.data_model.columnHeader(column)
3799 len = metrics.width(str(val) + "MM")
3800 max = len if len > max else max
3801 self.view.setColumnWidth(column, max)
3803 def ResizeColumnsToContents(self):
3804 n = min(self.data_model.child_count, 100)
3805 if n < 1:
3806 # No data yet, so connect a signal to notify when there is
3807 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3808 return
3809 columns = self.data_model.columnCount()
3810 for i in xrange(columns):
3811 self.ResizeColumnToContents(i, n)
3813 def UpdateColumnWidths(self, *x):
3814 # This only needs to be done once, so disconnect the signal now
3815 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3816 self.ResizeColumnsToContents()
3818 # Convert value to CSV
3820 def ToCSValue(val):
3821 if '"' in val:
3822 val = val.replace('"', '""')
3823 if "," in val or '"' in val:
3824 val = '"' + val + '"'
3825 return val
3827 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3829 glb_max_cols = 1000
3831 def RowColumnKey(a):
3832 return a.row() * glb_max_cols + a.column()
3834 # Copy selected table cells to clipboard
3836 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3837 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3838 idx_cnt = len(indexes)
3839 if not idx_cnt:
3840 return
3841 if idx_cnt == 1:
3842 with_hdr=False
3843 min_row = indexes[0].row()
3844 max_row = indexes[0].row()
3845 min_col = indexes[0].column()
3846 max_col = indexes[0].column()
3847 for i in indexes:
3848 min_row = min(min_row, i.row())
3849 max_row = max(max_row, i.row())
3850 min_col = min(min_col, i.column())
3851 max_col = max(max_col, i.column())
3852 if max_col > glb_max_cols:
3853 raise RuntimeError("glb_max_cols is too low")
3854 max_width = [0] * (1 + max_col - min_col)
3855 for i in indexes:
3856 c = i.column() - min_col
3857 max_width[c] = max(max_width[c], len(str(i.data())))
3858 text = ""
3859 pad = ""
3860 sep = ""
3861 if with_hdr:
3862 model = indexes[0].model()
3863 for col in range(min_col, max_col + 1):
3864 val = model.headerData(col, Qt.Horizontal)
3865 if as_csv:
3866 text += sep + ToCSValue(val)
3867 sep = ","
3868 else:
3869 c = col - min_col
3870 max_width[c] = max(max_width[c], len(val))
3871 width = max_width[c]
3872 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3873 if align & Qt.AlignRight:
3874 val = val.rjust(width)
3875 text += pad + sep + val
3876 pad = " " * (width - len(val))
3877 sep = " "
3878 text += "\n"
3879 pad = ""
3880 sep = ""
3881 last_row = min_row
3882 for i in indexes:
3883 if i.row() > last_row:
3884 last_row = i.row()
3885 text += "\n"
3886 pad = ""
3887 sep = ""
3888 if as_csv:
3889 text += sep + ToCSValue(str(i.data()))
3890 sep = ","
3891 else:
3892 width = max_width[i.column() - min_col]
3893 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3894 val = str(i.data()).rjust(width)
3895 else:
3896 val = str(i.data())
3897 text += pad + sep + val
3898 pad = " " * (width - len(val))
3899 sep = " "
3900 QApplication.clipboard().setText(text)
3902 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3903 indexes = view.selectedIndexes()
3904 if not len(indexes):
3905 return
3907 selection = view.selectionModel()
3909 first = None
3910 for i in indexes:
3911 above = view.indexAbove(i)
3912 if not selection.isSelected(above):
3913 first = i
3914 break
3916 if first is None:
3917 raise RuntimeError("CopyTreeCellsToClipboard internal error")
3919 model = first.model()
3920 row_cnt = 0
3921 col_cnt = model.columnCount(first)
3922 max_width = [0] * col_cnt
3924 indent_sz = 2
3925 indent_str = " " * indent_sz
3927 expanded_mark_sz = 2
3928 if sys.version_info[0] == 3:
3929 expanded_mark = "\u25BC "
3930 not_expanded_mark = "\u25B6 "
3931 else:
3932 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3933 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3934 leaf_mark = " "
3936 if not as_csv:
3937 pos = first
3938 while True:
3939 row_cnt += 1
3940 row = pos.row()
3941 for c in range(col_cnt):
3942 i = pos.sibling(row, c)
3943 if c:
3944 n = len(str(i.data()))
3945 else:
3946 n = len(str(i.data()).strip())
3947 n += (i.internalPointer().level - 1) * indent_sz
3948 n += expanded_mark_sz
3949 max_width[c] = max(max_width[c], n)
3950 pos = view.indexBelow(pos)
3951 if not selection.isSelected(pos):
3952 break
3954 text = ""
3955 pad = ""
3956 sep = ""
3957 if with_hdr:
3958 for c in range(col_cnt):
3959 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3960 if as_csv:
3961 text += sep + ToCSValue(val)
3962 sep = ","
3963 else:
3964 max_width[c] = max(max_width[c], len(val))
3965 width = max_width[c]
3966 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3967 if align & Qt.AlignRight:
3968 val = val.rjust(width)
3969 text += pad + sep + val
3970 pad = " " * (width - len(val))
3971 sep = " "
3972 text += "\n"
3973 pad = ""
3974 sep = ""
3976 pos = first
3977 while True:
3978 row = pos.row()
3979 for c in range(col_cnt):
3980 i = pos.sibling(row, c)
3981 val = str(i.data())
3982 if not c:
3983 if model.hasChildren(i):
3984 if view.isExpanded(i):
3985 mark = expanded_mark
3986 else:
3987 mark = not_expanded_mark
3988 else:
3989 mark = leaf_mark
3990 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
3991 if as_csv:
3992 text += sep + ToCSValue(val)
3993 sep = ","
3994 else:
3995 width = max_width[c]
3996 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3997 val = val.rjust(width)
3998 text += pad + sep + val
3999 pad = " " * (width - len(val))
4000 sep = " "
4001 pos = view.indexBelow(pos)
4002 if not selection.isSelected(pos):
4003 break
4004 text = text.rstrip() + "\n"
4005 pad = ""
4006 sep = ""
4008 QApplication.clipboard().setText(text)
4010 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4011 view.CopyCellsToClipboard(view, as_csv, with_hdr)
4013 def CopyCellsToClipboardHdr(view):
4014 CopyCellsToClipboard(view, False, True)
4016 def CopyCellsToClipboardCSV(view):
4017 CopyCellsToClipboard(view, True, True)
4019 # Context menu
4021 class ContextMenu(object):
4023 def __init__(self, view):
4024 self.view = view
4025 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4026 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4028 def ShowContextMenu(self, pos):
4029 menu = QMenu(self.view)
4030 self.AddActions(menu)
4031 menu.exec_(self.view.mapToGlobal(pos))
4033 def AddCopy(self, menu):
4034 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4035 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4037 def AddActions(self, menu):
4038 self.AddCopy(menu)
4040 class TreeContextMenu(ContextMenu):
4042 def __init__(self, view):
4043 super(TreeContextMenu, self).__init__(view)
4045 def AddActions(self, menu):
4046 i = self.view.currentIndex()
4047 text = str(i.data()).strip()
4048 if len(text):
4049 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4050 self.AddCopy(menu)
4052 # Table window
4054 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4056 def __init__(self, glb, table_name, parent=None):
4057 super(TableWindow, self).__init__(parent)
4059 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4061 self.model = QSortFilterProxyModel()
4062 self.model.setSourceModel(self.data_model)
4064 self.view = QTableView()
4065 self.view.setModel(self.model)
4066 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4067 self.view.verticalHeader().setVisible(False)
4068 self.view.sortByColumn(-1, Qt.AscendingOrder)
4069 self.view.setSortingEnabled(True)
4070 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4071 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4073 self.ResizeColumnsToContents()
4075 self.context_menu = ContextMenu(self.view)
4077 self.find_bar = FindBar(self, self, True)
4079 self.finder = ChildDataItemFinder(self.data_model)
4081 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4083 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4085 self.setWidget(self.vbox.Widget())
4087 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4089 def Find(self, value, direction, pattern, context):
4090 self.view.setFocus()
4091 self.find_bar.Busy()
4092 self.finder.Find(value, direction, pattern, context, self.FindDone)
4094 def FindDone(self, row):
4095 self.find_bar.Idle()
4096 if row >= 0:
4097 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4098 else:
4099 self.find_bar.NotFound()
4101 # Table list
4103 def GetTableList(glb):
4104 tables = []
4105 query = QSqlQuery(glb.db)
4106 if glb.dbref.is_sqlite3:
4107 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4108 else:
4109 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4110 while query.next():
4111 tables.append(query.value(0))
4112 if glb.dbref.is_sqlite3:
4113 tables.append("sqlite_master")
4114 else:
4115 tables.append("information_schema.tables")
4116 tables.append("information_schema.views")
4117 tables.append("information_schema.columns")
4118 return tables
4120 # Top Calls data model
4122 class TopCallsModel(SQLTableModel):
4124 def __init__(self, glb, report_vars, parent=None):
4125 text = ""
4126 if not glb.dbref.is_sqlite3:
4127 text = "::text"
4128 limit = ""
4129 if len(report_vars.limit):
4130 limit = " LIMIT " + report_vars.limit
4131 sql = ("SELECT comm, pid, tid, name,"
4132 " CASE"
4133 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4134 " ELSE short_name"
4135 " END AS dso,"
4136 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4137 " CASE"
4138 " WHEN (calls.flags = 1) THEN 'no call'" + text +
4139 " WHEN (calls.flags = 2) THEN 'no return'" + text +
4140 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4141 " ELSE ''" + text +
4142 " END AS flags"
4143 " FROM calls"
4144 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4145 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4146 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4147 " INNER JOIN comms ON calls.comm_id = comms.id"
4148 " INNER JOIN threads ON calls.thread_id = threads.id" +
4149 report_vars.where_clause +
4150 " ORDER BY elapsed_time DESC" +
4151 limit
4153 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4154 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4155 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4157 def columnAlignment(self, column):
4158 return self.alignment[column]
4160 # Top Calls report creation dialog
4162 class TopCallsDialog(ReportDialogBase):
4164 def __init__(self, glb, parent=None):
4165 title = "Top Calls by Elapsed Time"
4166 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4167 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4168 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4169 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4170 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4171 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4172 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4173 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4174 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4176 # Top Calls window
4178 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4180 def __init__(self, glb, report_vars, parent=None):
4181 super(TopCallsWindow, self).__init__(parent)
4183 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4184 self.model = self.data_model
4186 self.view = QTableView()
4187 self.view.setModel(self.model)
4188 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4189 self.view.verticalHeader().setVisible(False)
4190 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4191 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4193 self.context_menu = ContextMenu(self.view)
4195 self.ResizeColumnsToContents()
4197 self.find_bar = FindBar(self, self, True)
4199 self.finder = ChildDataItemFinder(self.model)
4201 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4203 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4205 self.setWidget(self.vbox.Widget())
4207 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4209 def Find(self, value, direction, pattern, context):
4210 self.view.setFocus()
4211 self.find_bar.Busy()
4212 self.finder.Find(value, direction, pattern, context, self.FindDone)
4214 def FindDone(self, row):
4215 self.find_bar.Idle()
4216 if row >= 0:
4217 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4218 else:
4219 self.find_bar.NotFound()
4221 # Action Definition
4223 def CreateAction(label, tip, callback, parent=None, shortcut=None):
4224 action = QAction(label, parent)
4225 if shortcut != None:
4226 action.setShortcuts(shortcut)
4227 action.setStatusTip(tip)
4228 action.triggered.connect(callback)
4229 return action
4231 # Typical application actions
4233 def CreateExitAction(app, parent=None):
4234 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4236 # Typical MDI actions
4238 def CreateCloseActiveWindowAction(mdi_area):
4239 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4241 def CreateCloseAllWindowsAction(mdi_area):
4242 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4244 def CreateTileWindowsAction(mdi_area):
4245 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4247 def CreateCascadeWindowsAction(mdi_area):
4248 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4250 def CreateNextWindowAction(mdi_area):
4251 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4253 def CreatePreviousWindowAction(mdi_area):
4254 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4256 # Typical MDI window menu
4258 class WindowMenu():
4260 def __init__(self, mdi_area, menu):
4261 self.mdi_area = mdi_area
4262 self.window_menu = menu.addMenu("&Windows")
4263 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4264 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4265 self.tile_windows = CreateTileWindowsAction(mdi_area)
4266 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4267 self.next_window = CreateNextWindowAction(mdi_area)
4268 self.previous_window = CreatePreviousWindowAction(mdi_area)
4269 self.window_menu.aboutToShow.connect(self.Update)
4271 def Update(self):
4272 self.window_menu.clear()
4273 sub_window_count = len(self.mdi_area.subWindowList())
4274 have_sub_windows = sub_window_count != 0
4275 self.close_active_window.setEnabled(have_sub_windows)
4276 self.close_all_windows.setEnabled(have_sub_windows)
4277 self.tile_windows.setEnabled(have_sub_windows)
4278 self.cascade_windows.setEnabled(have_sub_windows)
4279 self.next_window.setEnabled(have_sub_windows)
4280 self.previous_window.setEnabled(have_sub_windows)
4281 self.window_menu.addAction(self.close_active_window)
4282 self.window_menu.addAction(self.close_all_windows)
4283 self.window_menu.addSeparator()
4284 self.window_menu.addAction(self.tile_windows)
4285 self.window_menu.addAction(self.cascade_windows)
4286 self.window_menu.addSeparator()
4287 self.window_menu.addAction(self.next_window)
4288 self.window_menu.addAction(self.previous_window)
4289 if sub_window_count == 0:
4290 return
4291 self.window_menu.addSeparator()
4292 nr = 1
4293 for sub_window in self.mdi_area.subWindowList():
4294 label = str(nr) + " " + sub_window.name
4295 if nr < 10:
4296 label = "&" + label
4297 action = self.window_menu.addAction(label)
4298 action.setCheckable(True)
4299 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4300 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4301 self.window_menu.addAction(action)
4302 nr += 1
4304 def setActiveSubWindow(self, nr):
4305 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4307 # Help text
4309 glb_help_text = """
4310 <h1>Contents</h1>
4311 <style>
4312 p.c1 {
4313 text-indent: 40px;
4315 p.c2 {
4316 text-indent: 80px;
4319 </style>
4320 <p class=c1><a href=#reports>1. Reports</a></p>
4321 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4322 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4323 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
4324 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4325 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4326 <p class=c1><a href=#charts>2. Charts</a></p>
4327 <p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4328 <p class=c1><a href=#tables>3. Tables</a></p>
4329 <h1 id=reports>1. Reports</h1>
4330 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4331 The result is a GUI window with a tree representing a context-sensitive
4332 call-graph. Expanding a couple of levels of the tree and adjusting column
4333 widths to suit will display something like:
4334 <pre>
4335 Call Graph: pt_example
4336 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
4337 v- ls
4338 v- 2638:2638
4339 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
4340 |- unknown unknown 1 13198 0.1 1 0.0
4341 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
4342 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
4343 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
4344 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
4345 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
4346 >- __libc_csu_init ls 1 10354 0.1 10 0.0
4347 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
4348 v- main ls 1 8182043 99.6 180254 99.9
4349 </pre>
4350 <h3>Points to note:</h3>
4351 <ul>
4352 <li>The top level is a command name (comm)</li>
4353 <li>The next level is a thread (pid:tid)</li>
4354 <li>Subsequent levels are functions</li>
4355 <li>'Count' is the number of calls</li>
4356 <li>'Time' is the elapsed time until the function returns</li>
4357 <li>Percentages are relative to the level above</li>
4358 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
4359 </ul>
4360 <h3>Find</h3>
4361 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4362 The pattern matching symbols are ? for any character and * for zero or more characters.
4363 <h2 id=calltree>1.2 Call Tree</h2>
4364 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4365 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4366 <h2 id=allbranches>1.3 All branches</h2>
4367 The All branches report displays all branches in chronological order.
4368 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4369 <h3>Disassembly</h3>
4370 Open a branch to display disassembly. This only works if:
4371 <ol>
4372 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4373 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4374 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4375 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4376 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4377 </ol>
4378 <h4 id=xed>Intel XED Setup</h4>
4379 To use Intel XED, libxed.so must be present. To build and install libxed.so:
4380 <pre>
4381 git clone https://github.com/intelxed/mbuild.git mbuild
4382 git clone https://github.com/intelxed/xed
4383 cd xed
4384 ./mfile.py --share
4385 sudo ./mfile.py --prefix=/usr/local install
4386 sudo ldconfig
4387 </pre>
4388 <h3>Instructions per Cycle (IPC)</h3>
4389 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4390 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4391 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4392 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4393 since the previous displayed 'IPC'.
4394 <h3>Find</h3>
4395 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4396 Refer to Python documentation for the regular expression syntax.
4397 All columns are searched, but only currently fetched rows are searched.
4398 <h2 id=selectedbranches>1.4 Selected branches</h2>
4399 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4400 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4401 <h3>1.4.1 Time ranges</h3>
4402 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4403 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
4404 <pre>
4405 81073085947329-81073085958238 From 81073085947329 to 81073085958238
4406 100us-200us From 100us to 200us
4407 10ms- From 10ms to the end
4408 -100ns The first 100ns
4409 -10ms- The last 10ms
4410 </pre>
4411 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4412 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4413 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.
4414 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4415 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4416 <h1 id=charts>2. Charts</h1>
4417 <h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4418 This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4419 <h3>Features</h3>
4420 <ol>
4421 <li>Mouse over to highight the task and show the time</li>
4422 <li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4423 <li>Go back and forward by pressing the arrow buttons</li>
4424 <li>If call information is available, right-click to show a call tree opened to that task and time.
4425 Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4426 </li>
4427 </ol>
4428 <h3>Important</h3>
4429 The graph can be misleading in the following respects:
4430 <ol>
4431 <li>The graph shows the first task on each CPU as running from the beginning of the time range.
4432 Because tracing might start on different CPUs at different times, that is not necessarily the case.
4433 Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4434 <li>Similarly, the last task on each CPU can be showing running longer than it really was.
4435 Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4436 <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>
4437 </ol>
4438 <h1 id=tables>3. Tables</h1>
4439 The Tables menu shows all tables and views in the database. Most tables have an associated view
4440 which displays the information in a more friendly way. Not all data for large tables is fetched
4441 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4442 but that can be slow for large tables.
4443 <p>There are also tables of database meta-information.
4444 For SQLite3 databases, the sqlite_master table is included.
4445 For PostgreSQL databases, information_schema.tables/views/columns are included.
4446 <h3>Find</h3>
4447 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4448 Refer to Python documentation for the regular expression syntax.
4449 All columns are searched, but only currently fetched rows are searched.
4450 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4451 will go to the next/previous result in id order, instead of display order.
4454 # Help window
4456 class HelpWindow(QMdiSubWindow):
4458 def __init__(self, glb, parent=None):
4459 super(HelpWindow, self).__init__(parent)
4461 self.text = QTextBrowser()
4462 self.text.setHtml(glb_help_text)
4463 self.text.setReadOnly(True)
4464 self.text.setOpenExternalLinks(True)
4466 self.setWidget(self.text)
4468 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4470 # Main window that only displays the help text
4472 class HelpOnlyWindow(QMainWindow):
4474 def __init__(self, parent=None):
4475 super(HelpOnlyWindow, self).__init__(parent)
4477 self.setMinimumSize(200, 100)
4478 self.resize(800, 600)
4479 self.setWindowTitle("Exported SQL Viewer Help")
4480 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4482 self.text = QTextBrowser()
4483 self.text.setHtml(glb_help_text)
4484 self.text.setReadOnly(True)
4485 self.text.setOpenExternalLinks(True)
4487 self.setCentralWidget(self.text)
4489 # PostqreSQL server version
4491 def PostqreSQLServerVersion(db):
4492 query = QSqlQuery(db)
4493 QueryExec(query, "SELECT VERSION()")
4494 if query.next():
4495 v_str = query.value(0)
4496 v_list = v_str.strip().split(" ")
4497 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4498 return v_list[1]
4499 return v_str
4500 return "Unknown"
4502 # SQLite version
4504 def SQLiteVersion(db):
4505 query = QSqlQuery(db)
4506 QueryExec(query, "SELECT sqlite_version()")
4507 if query.next():
4508 return query.value(0)
4509 return "Unknown"
4511 # About dialog
4513 class AboutDialog(QDialog):
4515 def __init__(self, glb, parent=None):
4516 super(AboutDialog, self).__init__(parent)
4518 self.setWindowTitle("About Exported SQL Viewer")
4519 self.setMinimumWidth(300)
4521 pyside_version = "1" if pyside_version_1 else "2"
4523 text = "<pre>"
4524 text += "Python version: " + sys.version.split(" ")[0] + "\n"
4525 text += "PySide version: " + pyside_version + "\n"
4526 text += "Qt version: " + qVersion() + "\n"
4527 if glb.dbref.is_sqlite3:
4528 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
4529 else:
4530 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4531 text += "</pre>"
4533 self.text = QTextBrowser()
4534 self.text.setHtml(text)
4535 self.text.setReadOnly(True)
4536 self.text.setOpenExternalLinks(True)
4538 self.vbox = QVBoxLayout()
4539 self.vbox.addWidget(self.text)
4541 self.setLayout(self.vbox)
4543 # Font resize
4545 def ResizeFont(widget, diff):
4546 font = widget.font()
4547 sz = font.pointSize()
4548 font.setPointSize(sz + diff)
4549 widget.setFont(font)
4551 def ShrinkFont(widget):
4552 ResizeFont(widget, -1)
4554 def EnlargeFont(widget):
4555 ResizeFont(widget, 1)
4557 # Unique name for sub-windows
4559 def NumberedWindowName(name, nr):
4560 if nr > 1:
4561 name += " <" + str(nr) + ">"
4562 return name
4564 def UniqueSubWindowName(mdi_area, name):
4565 nr = 1
4566 while True:
4567 unique_name = NumberedWindowName(name, nr)
4568 ok = True
4569 for sub_window in mdi_area.subWindowList():
4570 if sub_window.name == unique_name:
4571 ok = False
4572 break
4573 if ok:
4574 return unique_name
4575 nr += 1
4577 # Add a sub-window
4579 def AddSubWindow(mdi_area, sub_window, name):
4580 unique_name = UniqueSubWindowName(mdi_area, name)
4581 sub_window.setMinimumSize(200, 100)
4582 sub_window.resize(800, 600)
4583 sub_window.setWindowTitle(unique_name)
4584 sub_window.setAttribute(Qt.WA_DeleteOnClose)
4585 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4586 sub_window.name = unique_name
4587 mdi_area.addSubWindow(sub_window)
4588 sub_window.show()
4590 # Main window
4592 class MainWindow(QMainWindow):
4594 def __init__(self, glb, parent=None):
4595 super(MainWindow, self).__init__(parent)
4597 self.glb = glb
4599 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4600 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4601 self.setMinimumSize(200, 100)
4603 self.mdi_area = QMdiArea()
4604 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4605 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4607 self.setCentralWidget(self.mdi_area)
4609 menu = self.menuBar()
4611 file_menu = menu.addMenu("&File")
4612 file_menu.addAction(CreateExitAction(glb.app, self))
4614 edit_menu = menu.addMenu("&Edit")
4615 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4616 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4617 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4618 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4619 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4620 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4622 reports_menu = menu.addMenu("&Reports")
4623 if IsSelectable(glb.db, "calls"):
4624 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4626 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4627 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4629 self.EventMenu(GetEventList(glb.db), reports_menu)
4631 if IsSelectable(glb.db, "calls"):
4632 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4634 if IsSelectable(glb.db, "context_switches"):
4635 charts_menu = menu.addMenu("&Charts")
4636 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4638 self.TableMenu(GetTableList(glb), menu)
4640 self.window_menu = WindowMenu(self.mdi_area, menu)
4642 help_menu = menu.addMenu("&Help")
4643 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4644 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4646 def Try(self, fn):
4647 win = self.mdi_area.activeSubWindow()
4648 if win:
4649 try:
4650 fn(win.view)
4651 except:
4652 pass
4654 def CopyToClipboard(self):
4655 self.Try(CopyCellsToClipboardHdr)
4657 def CopyToClipboardCSV(self):
4658 self.Try(CopyCellsToClipboardCSV)
4660 def Find(self):
4661 win = self.mdi_area.activeSubWindow()
4662 if win:
4663 try:
4664 win.find_bar.Activate()
4665 except:
4666 pass
4668 def FetchMoreRecords(self):
4669 win = self.mdi_area.activeSubWindow()
4670 if win:
4671 try:
4672 win.fetch_bar.Activate()
4673 except:
4674 pass
4676 def ShrinkFont(self):
4677 self.Try(ShrinkFont)
4679 def EnlargeFont(self):
4680 self.Try(EnlargeFont)
4682 def EventMenu(self, events, reports_menu):
4683 branches_events = 0
4684 for event in events:
4685 event = event.split(":")[0]
4686 if event == "branches":
4687 branches_events += 1
4688 dbid = 0
4689 for event in events:
4690 dbid += 1
4691 event = event.split(":")[0]
4692 if event == "branches":
4693 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4694 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4695 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4696 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4698 def TimeChartByCPU(self):
4699 TimeChartByCPUWindow(self.glb, self)
4701 def TableMenu(self, tables, menu):
4702 table_menu = menu.addMenu("&Tables")
4703 for table in tables:
4704 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4706 def NewCallGraph(self):
4707 CallGraphWindow(self.glb, self)
4709 def NewCallTree(self):
4710 CallTreeWindow(self.glb, self)
4712 def NewTopCalls(self):
4713 dialog = TopCallsDialog(self.glb, self)
4714 ret = dialog.exec_()
4715 if ret:
4716 TopCallsWindow(self.glb, dialog.report_vars, self)
4718 def NewBranchView(self, event_id):
4719 BranchWindow(self.glb, event_id, ReportVars(), self)
4721 def NewSelectedBranchView(self, event_id):
4722 dialog = SelectedBranchDialog(self.glb, self)
4723 ret = dialog.exec_()
4724 if ret:
4725 BranchWindow(self.glb, event_id, dialog.report_vars, self)
4727 def NewTableView(self, table_name):
4728 TableWindow(self.glb, table_name, self)
4730 def Help(self):
4731 HelpWindow(self.glb, self)
4733 def About(self):
4734 dialog = AboutDialog(self.glb, self)
4735 dialog.exec_()
4737 # XED Disassembler
4739 class xed_state_t(Structure):
4741 _fields_ = [
4742 ("mode", c_int),
4743 ("width", c_int)
4746 class XEDInstruction():
4748 def __init__(self, libxed):
4749 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4750 xedd_t = c_byte * 512
4751 self.xedd = xedd_t()
4752 self.xedp = addressof(self.xedd)
4753 libxed.xed_decoded_inst_zero(self.xedp)
4754 self.state = xed_state_t()
4755 self.statep = addressof(self.state)
4756 # Buffer for disassembled instruction text
4757 self.buffer = create_string_buffer(256)
4758 self.bufferp = addressof(self.buffer)
4760 class LibXED():
4762 def __init__(self):
4763 try:
4764 self.libxed = CDLL("libxed.so")
4765 except:
4766 self.libxed = None
4767 if not self.libxed:
4768 self.libxed = CDLL("/usr/local/lib/libxed.so")
4770 self.xed_tables_init = self.libxed.xed_tables_init
4771 self.xed_tables_init.restype = None
4772 self.xed_tables_init.argtypes = []
4774 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4775 self.xed_decoded_inst_zero.restype = None
4776 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4778 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4779 self.xed_operand_values_set_mode.restype = None
4780 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4782 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4783 self.xed_decoded_inst_zero_keep_mode.restype = None
4784 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4786 self.xed_decode = self.libxed.xed_decode
4787 self.xed_decode.restype = c_int
4788 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4790 self.xed_format_context = self.libxed.xed_format_context
4791 self.xed_format_context.restype = c_uint
4792 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4794 self.xed_tables_init()
4796 def Instruction(self):
4797 return XEDInstruction(self)
4799 def SetMode(self, inst, mode):
4800 if mode:
4801 inst.state.mode = 4 # 32-bit
4802 inst.state.width = 4 # 4 bytes
4803 else:
4804 inst.state.mode = 1 # 64-bit
4805 inst.state.width = 8 # 8 bytes
4806 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4808 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4809 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4810 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4811 if err:
4812 return 0, ""
4813 # Use AT&T mode (2), alternative is Intel (3)
4814 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4815 if not ok:
4816 return 0, ""
4817 if sys.version_info[0] == 2:
4818 result = inst.buffer.value
4819 else:
4820 result = inst.buffer.value.decode()
4821 # Return instruction length and the disassembled instruction text
4822 # For now, assume the length is in byte 166
4823 return inst.xedd[166], result
4825 def TryOpen(file_name):
4826 try:
4827 return open(file_name, "rb")
4828 except:
4829 return None
4831 def Is64Bit(f):
4832 result = sizeof(c_void_p)
4833 # ELF support only
4834 pos = f.tell()
4835 f.seek(0)
4836 header = f.read(7)
4837 f.seek(pos)
4838 magic = header[0:4]
4839 if sys.version_info[0] == 2:
4840 eclass = ord(header[4])
4841 encoding = ord(header[5])
4842 version = ord(header[6])
4843 else:
4844 eclass = header[4]
4845 encoding = header[5]
4846 version = header[6]
4847 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4848 result = True if eclass == 2 else False
4849 return result
4851 # Global data
4853 class Glb():
4855 def __init__(self, dbref, db, dbname):
4856 self.dbref = dbref
4857 self.db = db
4858 self.dbname = dbname
4859 self.home_dir = os.path.expanduser("~")
4860 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4861 if self.buildid_dir:
4862 self.buildid_dir += "/.build-id/"
4863 else:
4864 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4865 self.app = None
4866 self.mainwindow = None
4867 self.instances_to_shutdown_on_exit = weakref.WeakSet()
4868 try:
4869 self.disassembler = LibXED()
4870 self.have_disassembler = True
4871 except:
4872 self.have_disassembler = False
4873 self.host_machine_id = 0
4874 self.host_start_time = 0
4875 self.host_finish_time = 0
4877 def FileFromBuildId(self, build_id):
4878 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4879 return TryOpen(file_name)
4881 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4882 # Assume current machine i.e. no support for virtualization
4883 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4884 file_name = os.getenv("PERF_KCORE")
4885 f = TryOpen(file_name) if file_name else None
4886 if f:
4887 return f
4888 # For now, no special handling if long_name is /proc/kcore
4889 f = TryOpen(long_name)
4890 if f:
4891 return f
4892 f = self.FileFromBuildId(build_id)
4893 if f:
4894 return f
4895 return None
4897 def AddInstanceToShutdownOnExit(self, instance):
4898 self.instances_to_shutdown_on_exit.add(instance)
4900 # Shutdown any background processes or threads
4901 def ShutdownInstances(self):
4902 for x in self.instances_to_shutdown_on_exit:
4903 try:
4904 x.Shutdown()
4905 except:
4906 pass
4908 def GetHostMachineId(self):
4909 query = QSqlQuery(self.db)
4910 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4911 if query.next():
4912 self.host_machine_id = query.value(0)
4913 else:
4914 self.host_machine_id = 0
4915 return self.host_machine_id
4917 def HostMachineId(self):
4918 if self.host_machine_id:
4919 return self.host_machine_id
4920 return self.GetHostMachineId()
4922 def SelectValue(self, sql):
4923 query = QSqlQuery(self.db)
4924 try:
4925 QueryExec(query, sql)
4926 except:
4927 return None
4928 if query.next():
4929 return Decimal(query.value(0))
4930 return None
4932 def SwitchesMinTime(self, machine_id):
4933 return self.SelectValue("SELECT time"
4934 " FROM context_switches"
4935 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4936 " ORDER BY id LIMIT 1")
4938 def SwitchesMaxTime(self, machine_id):
4939 return self.SelectValue("SELECT time"
4940 " FROM context_switches"
4941 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4942 " ORDER BY id DESC LIMIT 1")
4944 def SamplesMinTime(self, machine_id):
4945 return self.SelectValue("SELECT time"
4946 " FROM samples"
4947 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4948 " ORDER BY id LIMIT 1")
4950 def SamplesMaxTime(self, machine_id):
4951 return self.SelectValue("SELECT time"
4952 " FROM samples"
4953 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4954 " ORDER BY id DESC LIMIT 1")
4956 def CallsMinTime(self, machine_id):
4957 return self.SelectValue("SELECT calls.call_time"
4958 " FROM calls"
4959 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4960 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4961 " ORDER BY calls.id LIMIT 1")
4963 def CallsMaxTime(self, machine_id):
4964 return self.SelectValue("SELECT calls.return_time"
4965 " FROM calls"
4966 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4967 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4968 " ORDER BY calls.return_time DESC LIMIT 1")
4970 def GetStartTime(self, machine_id):
4971 t0 = self.SwitchesMinTime(machine_id)
4972 t1 = self.SamplesMinTime(machine_id)
4973 t2 = self.CallsMinTime(machine_id)
4974 if t0 is None or (not(t1 is None) and t1 < t0):
4975 t0 = t1
4976 if t0 is None or (not(t2 is None) and t2 < t0):
4977 t0 = t2
4978 return t0
4980 def GetFinishTime(self, machine_id):
4981 t0 = self.SwitchesMaxTime(machine_id)
4982 t1 = self.SamplesMaxTime(machine_id)
4983 t2 = self.CallsMaxTime(machine_id)
4984 if t0 is None or (not(t1 is None) and t1 > t0):
4985 t0 = t1
4986 if t0 is None or (not(t2 is None) and t2 > t0):
4987 t0 = t2
4988 return t0
4990 def HostStartTime(self):
4991 if self.host_start_time:
4992 return self.host_start_time
4993 self.host_start_time = self.GetStartTime(self.HostMachineId())
4994 return self.host_start_time
4996 def HostFinishTime(self):
4997 if self.host_finish_time:
4998 return self.host_finish_time
4999 self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5000 return self.host_finish_time
5002 def StartTime(self, machine_id):
5003 if machine_id == self.HostMachineId():
5004 return self.HostStartTime()
5005 return self.GetStartTime(machine_id)
5007 def FinishTime(self, machine_id):
5008 if machine_id == self.HostMachineId():
5009 return self.HostFinishTime()
5010 return self.GetFinishTime(machine_id)
5012 # Database reference
5014 class DBRef():
5016 def __init__(self, is_sqlite3, dbname):
5017 self.is_sqlite3 = is_sqlite3
5018 self.dbname = dbname
5019 self.TRUE = "TRUE"
5020 self.FALSE = "FALSE"
5021 # SQLite prior to version 3.23 does not support TRUE and FALSE
5022 if self.is_sqlite3:
5023 self.TRUE = "1"
5024 self.FALSE = "0"
5026 def Open(self, connection_name):
5027 dbname = self.dbname
5028 if self.is_sqlite3:
5029 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5030 else:
5031 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5032 opts = dbname.split()
5033 for opt in opts:
5034 if "=" in opt:
5035 opt = opt.split("=")
5036 if opt[0] == "hostname":
5037 db.setHostName(opt[1])
5038 elif opt[0] == "port":
5039 db.setPort(int(opt[1]))
5040 elif opt[0] == "username":
5041 db.setUserName(opt[1])
5042 elif opt[0] == "password":
5043 db.setPassword(opt[1])
5044 elif opt[0] == "dbname":
5045 dbname = opt[1]
5046 else:
5047 dbname = opt
5049 db.setDatabaseName(dbname)
5050 if not db.open():
5051 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5052 return db, dbname
5054 # Main
5056 def Main():
5057 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5058 " or: exported-sql-viewer.py --help-only"
5059 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5060 ap.add_argument("--pyside-version-1", action='store_true')
5061 ap.add_argument("dbname", nargs="?")
5062 ap.add_argument("--help-only", action='store_true')
5063 args = ap.parse_args()
5065 if args.help_only:
5066 app = QApplication(sys.argv)
5067 mainwindow = HelpOnlyWindow()
5068 mainwindow.show()
5069 err = app.exec_()
5070 sys.exit(err)
5072 dbname = args.dbname
5073 if dbname is None:
5074 ap.print_usage()
5075 print("Too few arguments")
5076 sys.exit(1)
5078 is_sqlite3 = False
5079 try:
5080 f = open(dbname, "rb")
5081 if f.read(15) == b'SQLite format 3':
5082 is_sqlite3 = True
5083 f.close()
5084 except:
5085 pass
5087 dbref = DBRef(is_sqlite3, dbname)
5088 db, dbname = dbref.Open("main")
5089 glb = Glb(dbref, db, dbname)
5090 app = QApplication(sys.argv)
5091 glb.app = app
5092 mainwindow = MainWindow(glb)
5093 glb.mainwindow = mainwindow
5094 mainwindow.show()
5095 err = app.exec_()
5096 glb.ShutdownInstances()
5097 db.close()
5098 sys.exit(err)
5100 if __name__ == "__main__":
5101 Main()