2 # SPDX-License-Identifier: GPL-2.0
3 # exported-sql-viewer.py: view data from sql database
4 # Copyright (c) 2014-2018, Intel Corporation.
6 # To use this script you will need to have exported data using either the
7 # export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
10 # Following on from the example in the export scripts, a
11 # call-graph can be displayed for the pt_example database like this:
13 # python tools/perf/scripts/python/exported-sql-viewer.py pt_example
15 # Note that for PostgreSQL, this script supports connecting to remote databases
16 # by setting hostname, port, username, password, and dbname e.g.
18 # python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
20 # The result is a GUI window with a tree representing a context-sensitive
21 # call-graph. Expanding a couple of levels of the tree and adjusting column
22 # widths to suit will display something like:
24 # Call Graph: pt_example
25 # Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
28 # v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
29 # |- unknown unknown 1 13198 0.1 1 0.0
30 # >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
31 # >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
32 # v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
33 # >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
34 # >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
35 # >- __libc_csu_init ls 1 10354 0.1 10 0.0
36 # |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
37 # v- main ls 1 8182043 99.6 180254 99.9
40 # The top level is a command name (comm)
41 # The next level is a thread (pid:tid)
42 # Subsequent levels are functions
43 # 'Count' is the number of calls
44 # 'Time' is the elapsed time until the function returns
45 # Percentages are relative to the level above
46 # 'Branch Count' is the total number of branches for that function and all
47 # functions that it calls
49 # There is also a "All branches" report, which displays branches and
50 # possibly disassembly. However, presently, the only supported disassembler is
51 # Intel XED, and additionally the object code must be present in perf build ID
52 # cache. To use Intel XED, libxed.so must be present. To build and install
54 # git clone https://github.com/intelxed/mbuild.git mbuild
55 # git clone https://github.com/intelxed/xed
58 # sudo ./mfile.py --prefix=/usr/local install
63 # Time CPU Command PID TID Branch Type In Tx Branch
64 # 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65 # 7fab593ea260 48 89 e7 mov %rsp, %rdi
66 # 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67 # 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68 # 7fab593ea260 48 89 e7 mov %rsp, %rdi
69 # 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
70 # 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71 # 7fab593ea930 55 pushq %rbp
72 # 7fab593ea931 48 89 e5 mov %rsp, %rbp
73 # 7fab593ea934 41 57 pushq %r15
74 # 7fab593ea936 41 56 pushq %r14
75 # 7fab593ea938 41 55 pushq %r13
76 # 7fab593ea93a 41 54 pushq %r12
77 # 7fab593ea93c 53 pushq %rbx
78 # 7fab593ea93d 48 89 fb mov %rdi, %rbx
79 # 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
80 # 7fab593ea944 0f 31 rdtsc
81 # 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
82 # 7fab593ea94a 89 c0 mov %eax, %eax
83 # 7fab593ea94c 48 09 c2 or %rax, %rdx
84 # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
85 # 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86 # 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87 # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
88 # 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
89 # 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
91 from __future__ import print_function
99 import cPickle as pickle
100 # size of pickled integer big enough for record size
107 from PySide.QtCore import *
108 from PySide.QtGui import *
109 from PySide.QtSql import *
110 pyside_version_1 = True
111 from decimal import *
113 from multiprocessing import Process, Array, Value, Event
115 # xrange is range in Python3
121 def printerr(*args, **keyword_args):
122 print(*args, file=sys.stderr, **keyword_args)
124 # Data formatting helpers
133 return "+0x%x" % offset
137 if name == "[kernel.kallsyms]":
141 def findnth(s, sub, n, offs=0):
147 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
149 # Percent to one decimal place
151 def PercentToOneDP(n, d):
154 x = (n * Decimal(100)) / d
155 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
157 # Helper for queries that must not fail
159 def QueryExec(query, stmt):
160 ret = query.exec_(stmt)
162 raise Exception("Query failed: " + query.lastError().text())
166 class Thread(QThread):
168 done = Signal(object)
170 def __init__(self, task, param=None, parent=None):
171 super(Thread, self).__init__(parent)
177 if self.param is None:
178 done, result = self.task()
180 done, result = self.task(self.param)
181 self.done.emit(result)
187 class TreeModel(QAbstractItemModel):
189 def __init__(self, glb, parent=None):
190 super(TreeModel, self).__init__(parent)
192 self.root = self.GetRoot()
193 self.last_row_read = 0
195 def Item(self, parent):
197 return parent.internalPointer()
201 def rowCount(self, parent):
202 result = self.Item(parent).childCount()
205 self.dataChanged.emit(parent, parent)
208 def hasChildren(self, parent):
209 return self.Item(parent).hasChildren()
211 def headerData(self, section, orientation, role):
212 if role == Qt.TextAlignmentRole:
213 return self.columnAlignment(section)
214 if role != Qt.DisplayRole:
216 if orientation != Qt.Horizontal:
218 return self.columnHeader(section)
220 def parent(self, child):
221 child_item = child.internalPointer()
222 if child_item is self.root:
224 parent_item = child_item.getParentItem()
225 return self.createIndex(parent_item.getRow(), 0, parent_item)
227 def index(self, row, column, parent):
228 child_item = self.Item(parent).getChildItem(row)
229 return self.createIndex(row, column, child_item)
231 def DisplayData(self, item, index):
232 return item.getData(index.column())
234 def FetchIfNeeded(self, row):
235 if row > self.last_row_read:
236 self.last_row_read = row
237 if row + 10 >= self.root.child_count:
238 self.fetcher.Fetch(glb_chunk_sz)
240 def columnAlignment(self, column):
243 def columnFont(self, column):
246 def data(self, index, role):
247 if role == Qt.TextAlignmentRole:
248 return self.columnAlignment(index.column())
249 if role == Qt.FontRole:
250 return self.columnFont(index.column())
251 if role != Qt.DisplayRole:
253 item = index.internalPointer()
254 return self.DisplayData(item, index)
258 class TableModel(QAbstractTableModel):
260 def __init__(self, parent=None):
261 super(TableModel, self).__init__(parent)
263 self.child_items = []
264 self.last_row_read = 0
266 def Item(self, parent):
268 return parent.internalPointer()
272 def rowCount(self, parent):
273 return self.child_count
275 def headerData(self, section, orientation, role):
276 if role == Qt.TextAlignmentRole:
277 return self.columnAlignment(section)
278 if role != Qt.DisplayRole:
280 if orientation != Qt.Horizontal:
282 return self.columnHeader(section)
284 def index(self, row, column, parent):
285 return self.createIndex(row, column, self.child_items[row])
287 def DisplayData(self, item, index):
288 return item.getData(index.column())
290 def FetchIfNeeded(self, row):
291 if row > self.last_row_read:
292 self.last_row_read = row
293 if row + 10 >= self.child_count:
294 self.fetcher.Fetch(glb_chunk_sz)
296 def columnAlignment(self, column):
299 def columnFont(self, column):
302 def data(self, index, role):
303 if role == Qt.TextAlignmentRole:
304 return self.columnAlignment(index.column())
305 if role == Qt.FontRole:
306 return self.columnFont(index.column())
307 if role != Qt.DisplayRole:
309 item = index.internalPointer()
310 return self.DisplayData(item, index)
314 model_cache = weakref.WeakValueDictionary()
315 model_cache_lock = threading.Lock()
317 def LookupCreateModel(model_name, create_fn):
318 model_cache_lock.acquire()
320 model = model_cache[model_name]
325 model_cache[model_name] = model
326 model_cache_lock.release()
333 def __init__(self, parent, finder, is_reg_expr=False):
336 self.last_value = None
337 self.last_pattern = None
339 label = QLabel("Find:")
340 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
342 self.textbox = QComboBox()
343 self.textbox.setEditable(True)
344 self.textbox.currentIndexChanged.connect(self.ValueChanged)
346 self.progress = QProgressBar()
347 self.progress.setRange(0, 0)
351 self.pattern = QCheckBox("Regular Expression")
353 self.pattern = QCheckBox("Pattern")
354 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
356 self.next_button = QToolButton()
357 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
358 self.next_button.released.connect(lambda: self.NextPrev(1))
360 self.prev_button = QToolButton()
361 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
362 self.prev_button.released.connect(lambda: self.NextPrev(-1))
364 self.close_button = QToolButton()
365 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
366 self.close_button.released.connect(self.Deactivate)
368 self.hbox = QHBoxLayout()
369 self.hbox.setContentsMargins(0, 0, 0, 0)
371 self.hbox.addWidget(label)
372 self.hbox.addWidget(self.textbox)
373 self.hbox.addWidget(self.progress)
374 self.hbox.addWidget(self.pattern)
375 self.hbox.addWidget(self.next_button)
376 self.hbox.addWidget(self.prev_button)
377 self.hbox.addWidget(self.close_button)
380 self.bar.setLayout(self.hbox);
388 self.textbox.setFocus()
390 def Deactivate(self):
394 self.textbox.setEnabled(False)
396 self.next_button.hide()
397 self.prev_button.hide()
401 self.textbox.setEnabled(True)
404 self.next_button.show()
405 self.prev_button.show()
407 def Find(self, direction):
408 value = self.textbox.currentText()
409 pattern = self.pattern.isChecked()
410 self.last_value = value
411 self.last_pattern = pattern
412 self.finder.Find(value, direction, pattern, self.context)
414 def ValueChanged(self):
415 value = self.textbox.currentText()
416 pattern = self.pattern.isChecked()
417 index = self.textbox.currentIndex()
418 data = self.textbox.itemData(index)
419 # Store the pattern in the combo box to keep it with the text value
421 self.textbox.setItemData(index, pattern)
423 self.pattern.setChecked(data)
426 def NextPrev(self, direction):
427 value = self.textbox.currentText()
428 pattern = self.pattern.isChecked()
429 if value != self.last_value:
430 index = self.textbox.findText(value)
431 # Allow for a button press before the value has been added to the combo box
433 index = self.textbox.count()
434 self.textbox.addItem(value, pattern)
435 self.textbox.setCurrentIndex(index)
438 self.textbox.setItemData(index, pattern)
439 elif pattern != self.last_pattern:
440 # Keep the pattern recorded in the combo box up to date
441 index = self.textbox.currentIndex()
442 self.textbox.setItemData(index, pattern)
446 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
448 # Context-sensitive call graph data model item base
450 class CallGraphLevelItemBase(object):
452 def __init__(self, glb, row, parent_item):
455 self.parent_item = parent_item
456 self.query_done = False;
458 self.child_items = []
460 def getChildItem(self, row):
461 return self.child_items[row]
463 def getParentItem(self):
464 return self.parent_item
469 def childCount(self):
470 if not self.query_done:
472 if not self.child_count:
474 return self.child_count
476 def hasChildren(self):
477 if not self.query_done:
479 return self.child_count > 0
481 def getData(self, column):
482 return self.data[column]
484 # Context-sensitive call graph data model level 2+ item base
486 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
488 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
489 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
490 self.comm_id = comm_id
491 self.thread_id = thread_id
492 self.call_path_id = call_path_id
493 self.branch_count = branch_count
497 self.query_done = True;
498 query = QSqlQuery(self.glb.db)
499 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
501 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
502 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
503 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
504 " WHERE parent_call_path_id = " + str(self.call_path_id) +
505 " AND comm_id = " + str(self.comm_id) +
506 " AND thread_id = " + str(self.thread_id) +
507 " GROUP BY call_path_id, name, short_name"
508 " ORDER BY call_path_id")
510 child_item = CallGraphLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
511 self.child_items.append(child_item)
512 self.child_count += 1
514 # Context-sensitive call graph data model level three item
516 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
518 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
519 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
521 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
522 self.dbid = call_path_id
524 # Context-sensitive call graph data model level two item
526 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
528 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
529 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
530 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
531 self.dbid = thread_id
534 super(CallGraphLevelTwoItem, self).Select()
535 for child_item in self.child_items:
536 self.time += child_item.time
537 self.branch_count += child_item.branch_count
538 for child_item in self.child_items:
539 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
540 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
542 # Context-sensitive call graph data model level one item
544 class CallGraphLevelOneItem(CallGraphLevelItemBase):
546 def __init__(self, glb, row, comm_id, comm, parent_item):
547 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
548 self.data = [comm, "", "", "", "", "", ""]
552 self.query_done = True;
553 query = QSqlQuery(self.glb.db)
554 QueryExec(query, "SELECT thread_id, pid, tid"
556 " INNER JOIN threads ON thread_id = threads.id"
557 " WHERE comm_id = " + str(self.dbid))
559 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
560 self.child_items.append(child_item)
561 self.child_count += 1
563 # Context-sensitive call graph data model root item
565 class CallGraphRootItem(CallGraphLevelItemBase):
567 def __init__(self, glb):
568 super(CallGraphRootItem, self).__init__(glb, 0, None)
570 self.query_done = True;
571 query = QSqlQuery(glb.db)
572 QueryExec(query, "SELECT id, comm FROM comms")
574 if not query.value(0):
576 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
577 self.child_items.append(child_item)
578 self.child_count += 1
580 # Context-sensitive call graph data model base
582 class CallGraphModelBase(TreeModel):
584 def __init__(self, glb, parent=None):
585 super(CallGraphModelBase, self).__init__(glb, parent)
587 def FindSelect(self, value, pattern, query):
589 # postgresql and sqlite pattern patching differences:
590 # postgresql LIKE is case sensitive but sqlite LIKE is not
591 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
592 # postgresql supports ILIKE which is case insensitive
593 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
594 if not self.glb.dbref.is_sqlite3:
596 s = value.replace("%", "\%")
597 s = s.replace("_", "\_")
598 # Translate * and ? into SQL LIKE pattern characters % and _
599 trans = string.maketrans("*?", "%_")
600 match = " LIKE '" + str(s).translate(trans) + "'"
602 match = " GLOB '" + str(value) + "'"
604 match = " = '" + str(value) + "'"
605 self.DoFindSelect(query, match)
607 def Found(self, query, found):
609 return self.FindPath(query)
612 def FindValue(self, value, pattern, query, last_value, last_pattern):
613 if last_value == value and pattern == last_pattern:
614 found = query.first()
616 self.FindSelect(value, pattern, query)
618 return self.Found(query, found)
620 def FindNext(self, query):
623 found = query.first()
624 return self.Found(query, found)
626 def FindPrev(self, query):
627 found = query.previous()
630 return self.Found(query, found)
632 def FindThread(self, c):
633 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
634 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
635 elif c.direction > 0:
636 ids = self.FindNext(c.query)
638 ids = self.FindPrev(c.query)
641 def Find(self, value, direction, pattern, context, callback):
643 def __init__(self, *x):
644 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
645 def Update(self, *x):
646 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
648 context[0].Update(value, direction, pattern)
650 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
651 # Use a thread so the UI is not blocked during the SELECT
652 thread = Thread(self.FindThread, context[0])
653 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
656 def FindDone(self, thread, callback, ids):
659 # Context-sensitive call graph data model
661 class CallGraphModel(CallGraphModelBase):
663 def __init__(self, glb, parent=None):
664 super(CallGraphModel, self).__init__(glb, parent)
667 return CallGraphRootItem(self.glb)
669 def columnCount(self, parent=None):
672 def columnHeader(self, column):
673 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
674 return headers[column]
676 def columnAlignment(self, column):
677 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
678 return alignment[column]
680 def DoFindSelect(self, query, match):
681 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
683 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
684 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
685 " WHERE symbols.name" + match +
686 " GROUP BY comm_id, thread_id, call_path_id"
687 " ORDER BY comm_id, thread_id, call_path_id")
689 def FindPath(self, query):
690 # Turn the query result into a list of ids that the tree view can walk
691 # to open the tree at the right place.
693 parent_id = query.value(0)
695 ids.insert(0, parent_id)
696 q2 = QSqlQuery(self.glb.db)
697 QueryExec(q2, "SELECT parent_id"
699 " WHERE id = " + str(parent_id))
702 parent_id = q2.value(0)
703 # The call path root is not used
706 ids.insert(0, query.value(2))
707 ids.insert(0, query.value(1))
710 # Call tree data model level 2+ item base
712 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
714 def __init__(self, glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
715 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
716 self.comm_id = comm_id
717 self.thread_id = thread_id
718 self.calls_id = calls_id
719 self.branch_count = branch_count
723 self.query_done = True;
724 if self.calls_id == 0:
725 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
728 query = QSqlQuery(self.glb.db)
729 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
731 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
732 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
733 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
734 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
735 " ORDER BY call_time, calls.id")
737 child_item = CallTreeLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
738 self.child_items.append(child_item)
739 self.child_count += 1
741 # Call tree data model level three item
743 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
745 def __init__(self, glb, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
746 super(CallTreeLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
748 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
751 # Call tree data model level two item
753 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
755 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
756 super(CallTreeLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 0, 0, 0, parent_item)
757 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
758 self.dbid = thread_id
761 super(CallTreeLevelTwoItem, self).Select()
762 for child_item in self.child_items:
763 self.time += child_item.time
764 self.branch_count += child_item.branch_count
765 for child_item in self.child_items:
766 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
767 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
769 # Call tree data model level one item
771 class CallTreeLevelOneItem(CallGraphLevelItemBase):
773 def __init__(self, glb, row, comm_id, comm, parent_item):
774 super(CallTreeLevelOneItem, self).__init__(glb, row, parent_item)
775 self.data = [comm, "", "", "", "", "", ""]
779 self.query_done = True;
780 query = QSqlQuery(self.glb.db)
781 QueryExec(query, "SELECT thread_id, pid, tid"
783 " INNER JOIN threads ON thread_id = threads.id"
784 " WHERE comm_id = " + str(self.dbid))
786 child_item = CallTreeLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
787 self.child_items.append(child_item)
788 self.child_count += 1
790 # Call tree data model root item
792 class CallTreeRootItem(CallGraphLevelItemBase):
794 def __init__(self, glb):
795 super(CallTreeRootItem, self).__init__(glb, 0, None)
797 self.query_done = True;
798 query = QSqlQuery(glb.db)
799 QueryExec(query, "SELECT id, comm FROM comms")
801 if not query.value(0):
803 child_item = CallTreeLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
804 self.child_items.append(child_item)
805 self.child_count += 1
807 # Call Tree data model
809 class CallTreeModel(CallGraphModelBase):
811 def __init__(self, glb, parent=None):
812 super(CallTreeModel, self).__init__(glb, parent)
815 return CallTreeRootItem(self.glb)
817 def columnCount(self, parent=None):
820 def columnHeader(self, column):
821 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
822 return headers[column]
824 def columnAlignment(self, column):
825 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
826 return alignment[column]
828 def DoFindSelect(self, query, match):
829 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
831 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
832 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
833 " WHERE symbols.name" + match +
834 " ORDER BY comm_id, thread_id, call_time, calls.id")
836 def FindPath(self, query):
837 # Turn the query result into a list of ids that the tree view can walk
838 # to open the tree at the right place.
840 parent_id = query.value(0)
842 ids.insert(0, parent_id)
843 q2 = QSqlQuery(self.glb.db)
844 QueryExec(q2, "SELECT parent_id"
846 " WHERE id = " + str(parent_id))
849 parent_id = q2.value(0)
850 ids.insert(0, query.value(2))
851 ids.insert(0, query.value(1))
854 # Vertical widget layout
858 def __init__(self, w1, w2, w3=None):
859 self.vbox = QWidget()
860 self.vbox.setLayout(QVBoxLayout());
862 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
864 self.vbox.layout().addWidget(w1)
865 self.vbox.layout().addWidget(w2)
867 self.vbox.layout().addWidget(w3)
874 class TreeWindowBase(QMdiSubWindow):
876 def __init__(self, parent=None):
877 super(TreeWindowBase, self).__init__(parent)
883 def DisplayFound(self, ids):
886 parent = QModelIndex()
889 n = self.model.rowCount(parent)
890 for row in xrange(n):
891 child = self.model.index(row, 0, parent)
892 if child.internalPointer().dbid == dbid:
894 self.view.setCurrentIndex(child)
901 def Find(self, value, direction, pattern, context):
904 self.model.Find(value, direction, pattern, context, self.FindDone)
906 def FindDone(self, ids):
908 if not self.DisplayFound(ids):
912 self.find_bar.NotFound()
915 # Context-sensitive call graph window
917 class CallGraphWindow(TreeWindowBase):
919 def __init__(self, glb, parent=None):
920 super(CallGraphWindow, self).__init__(parent)
922 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
924 self.view = QTreeView()
925 self.view.setModel(self.model)
927 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
928 self.view.setColumnWidth(c, w)
930 self.find_bar = FindBar(self, self)
932 self.vbox = VBox(self.view, self.find_bar.Widget())
934 self.setWidget(self.vbox.Widget())
936 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
940 class CallTreeWindow(TreeWindowBase):
942 def __init__(self, glb, parent=None):
943 super(CallTreeWindow, self).__init__(parent)
945 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
947 self.view = QTreeView()
948 self.view.setModel(self.model)
950 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
951 self.view.setColumnWidth(c, w)
953 self.find_bar = FindBar(self, self)
955 self.vbox = VBox(self.view, self.find_bar.Widget())
957 self.setWidget(self.vbox.Widget())
959 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
961 # Child data item finder
963 class ChildDataItemFinder():
965 def __init__(self, root):
967 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
971 def FindSelect(self):
974 pattern = re.compile(self.value)
975 for child in self.root.child_items:
976 for column_data in child.data:
977 if re.search(pattern, str(column_data)) is not None:
978 self.rows.append(child.row)
981 for child in self.root.child_items:
982 for column_data in child.data:
983 if self.value in str(column_data):
984 self.rows.append(child.row)
989 if self.last_value != self.value or self.pattern != self.last_pattern:
991 if not len(self.rows):
993 return self.rows[self.pos]
995 def FindThread(self):
996 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
997 row = self.FindValue()
999 if self.direction > 0:
1001 if self.pos >= len(self.rows):
1006 self.pos = len(self.rows) - 1
1007 row = self.rows[self.pos]
1012 def Find(self, value, direction, pattern, context, callback):
1013 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1014 # Use a thread so the UI is not blocked
1015 thread = Thread(self.FindThread)
1016 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1019 def FindDone(self, thread, callback, row):
1022 # Number of database records to fetch in one go
1024 glb_chunk_sz = 10000
1026 # Background process for SQL data fetcher
1028 class SQLFetcherProcess():
1030 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1031 # Need a unique connection name
1032 conn_name = "SQLFetcher" + str(os.getpid())
1033 self.db, dbname = dbref.Open(conn_name)
1035 self.buffer = buffer
1038 self.fetch_count = fetch_count
1039 self.fetching_done = fetching_done
1040 self.process_target = process_target
1041 self.wait_event = wait_event
1042 self.fetched_event = fetched_event
1044 self.query = QSqlQuery(self.db)
1045 self.query_limit = 0 if "$$last_id$$" in sql else 2
1049 self.local_head = self.head.value
1050 self.local_tail = self.tail.value
1053 if self.query_limit:
1054 if self.query_limit == 1:
1056 self.query_limit -= 1
1057 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1058 QueryExec(self.query, stmt)
1061 if not self.query.next():
1063 if not self.query.next():
1065 self.last_id = self.query.value(0)
1066 return self.prep(self.query)
1068 def WaitForTarget(self):
1070 self.wait_event.clear()
1071 target = self.process_target.value
1072 if target > self.fetched or target < 0:
1074 self.wait_event.wait()
1077 def HasSpace(self, sz):
1078 if self.local_tail <= self.local_head:
1079 space = len(self.buffer) - self.local_head
1082 if space >= glb_nsz:
1083 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1084 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
1085 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1087 if self.local_tail - self.local_head > sz:
1091 def WaitForSpace(self, sz):
1092 if self.HasSpace(sz):
1095 self.wait_event.clear()
1096 self.local_tail = self.tail.value
1097 if self.HasSpace(sz):
1099 self.wait_event.wait()
1101 def AddToBuffer(self, obj):
1102 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
1104 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
1106 self.WaitForSpace(sz)
1107 pos = self.local_head
1108 self.buffer[pos : pos + len(nd)] = nd
1109 self.buffer[pos + glb_nsz : pos + sz] = d
1110 self.local_head += sz
1112 def FetchBatch(self, batch_size):
1114 while batch_size > fetched:
1119 self.AddToBuffer(obj)
1122 self.fetched += fetched
1123 with self.fetch_count.get_lock():
1124 self.fetch_count.value += fetched
1125 self.head.value = self.local_head
1126 self.fetched_event.set()
1130 target = self.WaitForTarget()
1133 batch_size = min(glb_chunk_sz, target - self.fetched)
1134 self.FetchBatch(batch_size)
1135 self.fetching_done.value = True
1136 self.fetched_event.set()
1138 def SQLFetcherFn(*x):
1139 process = SQLFetcherProcess(*x)
1144 class SQLFetcher(QObject):
1146 done = Signal(object)
1148 def __init__(self, glb, sql, prep, process_data, parent=None):
1149 super(SQLFetcher, self).__init__(parent)
1150 self.process_data = process_data
1153 self.last_target = 0
1155 self.buffer_size = 16 * 1024 * 1024
1156 self.buffer = Array(c_char, self.buffer_size, lock=False)
1157 self.head = Value(c_longlong)
1158 self.tail = Value(c_longlong)
1160 self.fetch_count = Value(c_longlong)
1161 self.fetching_done = Value(c_bool)
1163 self.process_target = Value(c_longlong)
1164 self.wait_event = Event()
1165 self.fetched_event = Event()
1166 glb.AddInstanceToShutdownOnExit(self)
1167 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))
1168 self.process.start()
1169 self.thread = Thread(self.Thread)
1170 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1174 # Tell the thread and process to exit
1175 self.process_target.value = -1
1176 self.wait_event.set()
1178 self.fetching_done.value = True
1179 self.fetched_event.set()
1185 self.fetched_event.clear()
1186 fetch_count = self.fetch_count.value
1187 if fetch_count != self.last_count:
1189 if self.fetching_done.value:
1192 self.fetched_event.wait()
1193 count = fetch_count - self.last_count
1194 self.last_count = fetch_count
1195 self.fetched += count
1198 def Fetch(self, nr):
1200 # -1 inidcates there are no more
1202 result = self.fetched
1203 extra = result + nr - self.target
1205 self.target += extra
1206 # process_target < 0 indicates shutting down
1207 if self.process_target.value >= 0:
1208 self.process_target.value = self.target
1209 self.wait_event.set()
1212 def RemoveFromBuffer(self):
1213 pos = self.local_tail
1214 if len(self.buffer) - pos < glb_nsz:
1216 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
1219 n = pickle.loads(self.buffer[0 : glb_nsz])
1221 obj = pickle.loads(self.buffer[pos : pos + n])
1222 self.local_tail = pos + n
1225 def ProcessData(self, count):
1226 for i in xrange(count):
1227 obj = self.RemoveFromBuffer()
1228 self.process_data(obj)
1229 self.tail.value = self.local_tail
1230 self.wait_event.set()
1231 self.done.emit(count)
1233 # Fetch more records bar
1235 class FetchMoreRecordsBar():
1237 def __init__(self, model, parent):
1240 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1241 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1243 self.fetch_count = QSpinBox()
1244 self.fetch_count.setRange(1, 1000000)
1245 self.fetch_count.setValue(10)
1246 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1248 self.fetch = QPushButton("Go!")
1249 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1250 self.fetch.released.connect(self.FetchMoreRecords)
1252 self.progress = QProgressBar()
1253 self.progress.setRange(0, 100)
1254 self.progress.hide()
1256 self.done_label = QLabel("All records fetched")
1257 self.done_label.hide()
1259 self.spacer = QLabel("")
1261 self.close_button = QToolButton()
1262 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1263 self.close_button.released.connect(self.Deactivate)
1265 self.hbox = QHBoxLayout()
1266 self.hbox.setContentsMargins(0, 0, 0, 0)
1268 self.hbox.addWidget(self.label)
1269 self.hbox.addWidget(self.fetch_count)
1270 self.hbox.addWidget(self.fetch)
1271 self.hbox.addWidget(self.spacer)
1272 self.hbox.addWidget(self.progress)
1273 self.hbox.addWidget(self.done_label)
1274 self.hbox.addWidget(self.close_button)
1276 self.bar = QWidget()
1277 self.bar.setLayout(self.hbox);
1280 self.in_progress = False
1281 self.model.progress.connect(self.Progress)
1285 if not model.HasMoreRecords():
1293 self.fetch.setFocus()
1295 def Deactivate(self):
1298 def Enable(self, enable):
1299 self.fetch.setEnabled(enable)
1300 self.fetch_count.setEnabled(enable)
1306 self.progress.show()
1309 self.in_progress = False
1311 self.progress.hide()
1316 return self.fetch_count.value() * glb_chunk_sz
1322 self.fetch_count.hide()
1325 self.done_label.show()
1327 def Progress(self, count):
1328 if self.in_progress:
1330 percent = ((count - self.start) * 100) / self.Target()
1334 self.progress.setValue(percent)
1336 # Count value of zero means no more records
1339 def FetchMoreRecords(self):
1342 self.progress.setValue(0)
1344 self.in_progress = True
1345 self.start = self.model.FetchMoreRecords(self.Target())
1347 # Brance data model level two item
1349 class BranchLevelTwoItem():
1351 def __init__(self, row, text, parent_item):
1353 self.parent_item = parent_item
1354 self.data = [""] * 8
1358 def getParentItem(self):
1359 return self.parent_item
1364 def childCount(self):
1367 def hasChildren(self):
1370 def getData(self, column):
1371 return self.data[column]
1373 # Brance data model level one item
1375 class BranchLevelOneItem():
1377 def __init__(self, glb, row, data, parent_item):
1380 self.parent_item = parent_item
1381 self.child_count = 0
1382 self.child_items = []
1383 self.data = data[1:]
1386 self.query_done = False
1388 def getChildItem(self, row):
1389 return self.child_items[row]
1391 def getParentItem(self):
1392 return self.parent_item
1398 self.query_done = True
1400 if not self.glb.have_disassembler:
1403 query = QSqlQuery(self.glb.db)
1405 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1407 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1408 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1409 " WHERE samples.id = " + str(self.dbid))
1410 if not query.next():
1412 cpu = query.value(0)
1413 dso = query.value(1)
1414 sym = query.value(2)
1415 if dso == 0 or sym == 0:
1417 off = query.value(3)
1418 short_name = query.value(4)
1419 long_name = query.value(5)
1420 build_id = query.value(6)
1421 sym_start = query.value(7)
1424 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1426 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1427 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1428 " ORDER BY samples.id"
1430 if not query.next():
1432 if query.value(0) != dso:
1433 # Cannot disassemble from one dso to another
1435 bsym = query.value(1)
1436 boff = query.value(2)
1437 bsym_start = query.value(3)
1440 tot = bsym_start + boff + 1 - sym_start - off
1441 if tot <= 0 or tot > 16384:
1444 inst = self.glb.disassembler.Instruction()
1445 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1448 mode = 0 if Is64Bit(f) else 1
1449 self.glb.disassembler.SetMode(inst, mode)
1452 buf = create_string_buffer(tot + 16)
1453 f.seek(sym_start + off)
1454 buf.value = f.read(buf_sz)
1455 buf_ptr = addressof(buf)
1458 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1460 byte_str = tohex(ip).rjust(16)
1461 for k in xrange(cnt):
1462 byte_str += " %02x" % ord(buf[i])
1467 self.child_items.append(BranchLevelTwoItem(0, byte_str + " " + text, self))
1468 self.child_count += 1
1476 def childCount(self):
1477 if not self.query_done:
1479 if not self.child_count:
1481 return self.child_count
1483 def hasChildren(self):
1484 if not self.query_done:
1486 return self.child_count > 0
1488 def getData(self, column):
1489 return self.data[column]
1491 # Brance data model root item
1493 class BranchRootItem():
1496 self.child_count = 0
1497 self.child_items = []
1500 def getChildItem(self, row):
1501 return self.child_items[row]
1503 def getParentItem(self):
1509 def childCount(self):
1510 return self.child_count
1512 def hasChildren(self):
1513 return self.child_count > 0
1515 def getData(self, column):
1518 # Branch data preparation
1520 def BranchDataPrep(query):
1522 for i in xrange(0, 8):
1523 data.append(query.value(i))
1524 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1525 " (" + dsoname(query.value(11)) + ")" + " -> " +
1526 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1527 " (" + dsoname(query.value(15)) + ")")
1530 def BranchDataPrepWA(query):
1532 data.append(query.value(0))
1533 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1534 data.append("{:>19}".format(query.value(1)))
1535 for i in xrange(2, 8):
1536 data.append(query.value(i))
1537 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1538 " (" + dsoname(query.value(11)) + ")" + " -> " +
1539 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1540 " (" + dsoname(query.value(15)) + ")")
1545 class BranchModel(TreeModel):
1547 progress = Signal(object)
1549 def __init__(self, glb, event_id, where_clause, parent=None):
1550 super(BranchModel, self).__init__(glb, parent)
1551 self.event_id = event_id
1554 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1555 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1556 " ip, symbols.name, sym_offset, dsos.short_name,"
1557 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1559 " INNER JOIN comms ON comm_id = comms.id"
1560 " INNER JOIN threads ON thread_id = threads.id"
1561 " INNER JOIN branch_types ON branch_type = branch_types.id"
1562 " INNER JOIN symbols ON symbol_id = symbols.id"
1563 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1564 " INNER JOIN dsos ON samples.dso_id = dsos.id"
1565 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1566 " WHERE samples.id > $$last_id$$" + where_clause +
1567 " AND evsel_id = " + str(self.event_id) +
1568 " ORDER BY samples.id"
1569 " LIMIT " + str(glb_chunk_sz))
1570 if pyside_version_1 and sys.version_info[0] == 3:
1571 prep = BranchDataPrepWA
1573 prep = BranchDataPrep
1574 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
1575 self.fetcher.done.connect(self.Update)
1576 self.fetcher.Fetch(glb_chunk_sz)
1579 return BranchRootItem()
1581 def columnCount(self, parent=None):
1584 def columnHeader(self, column):
1585 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1587 def columnFont(self, column):
1590 return QFont("Monospace")
1592 def DisplayData(self, item, index):
1594 self.FetchIfNeeded(item.row)
1595 return item.getData(index.column())
1597 def AddSample(self, data):
1598 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1599 self.root.child_items.append(child)
1602 def Update(self, fetched):
1605 self.progress.emit(0)
1606 child_count = self.root.child_count
1607 count = self.populated - child_count
1609 parent = QModelIndex()
1610 self.beginInsertRows(parent, child_count, child_count + count - 1)
1611 self.insertRows(child_count, count, parent)
1612 self.root.child_count += count
1613 self.endInsertRows()
1614 self.progress.emit(self.root.child_count)
1616 def FetchMoreRecords(self, count):
1617 current = self.root.child_count
1619 self.fetcher.Fetch(count)
1621 self.progress.emit(0)
1624 def HasMoreRecords(self):
1631 def __init__(self, name = "", where_clause = "", limit = ""):
1633 self.where_clause = where_clause
1637 return str(self.where_clause + ";" + self.limit)
1641 class BranchWindow(QMdiSubWindow):
1643 def __init__(self, glb, event_id, report_vars, parent=None):
1644 super(BranchWindow, self).__init__(parent)
1646 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
1648 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1650 self.view = QTreeView()
1651 self.view.setUniformRowHeights(True)
1652 self.view.setModel(self.model)
1654 self.ResizeColumnsToContents()
1656 self.find_bar = FindBar(self, self, True)
1658 self.finder = ChildDataItemFinder(self.model.root)
1660 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1662 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1664 self.setWidget(self.vbox.Widget())
1666 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1668 def ResizeColumnToContents(self, column, n):
1669 # Using the view's resizeColumnToContents() here is extrememly slow
1670 # so implement a crude alternative
1671 mm = "MM" if column else "MMMM"
1672 font = self.view.font()
1673 metrics = QFontMetrics(font)
1675 for row in xrange(n):
1676 val = self.model.root.child_items[row].data[column]
1677 len = metrics.width(str(val) + mm)
1678 max = len if len > max else max
1679 val = self.model.columnHeader(column)
1680 len = metrics.width(str(val) + mm)
1681 max = len if len > max else max
1682 self.view.setColumnWidth(column, max)
1684 def ResizeColumnsToContents(self):
1685 n = min(self.model.root.child_count, 100)
1687 # No data yet, so connect a signal to notify when there is
1688 self.model.rowsInserted.connect(self.UpdateColumnWidths)
1690 columns = self.model.columnCount()
1691 for i in xrange(columns):
1692 self.ResizeColumnToContents(i, n)
1694 def UpdateColumnWidths(self, *x):
1695 # This only needs to be done once, so disconnect the signal now
1696 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1697 self.ResizeColumnsToContents()
1699 def Find(self, value, direction, pattern, context):
1700 self.view.setFocus()
1701 self.find_bar.Busy()
1702 self.finder.Find(value, direction, pattern, context, self.FindDone)
1704 def FindDone(self, row):
1705 self.find_bar.Idle()
1707 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1709 self.find_bar.NotFound()
1711 # Line edit data item
1713 class LineEditDataItem(object):
1715 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1718 self.placeholder_text = placeholder_text
1719 self.parent = parent
1722 self.value = default
1724 self.widget = QLineEdit(default)
1725 self.widget.editingFinished.connect(self.Validate)
1726 self.widget.textChanged.connect(self.Invalidate)
1729 self.validated = True
1731 if placeholder_text:
1732 self.widget.setPlaceholderText(placeholder_text)
1734 def TurnTextRed(self):
1736 palette = QPalette()
1737 palette.setColor(QPalette.Text,Qt.red)
1738 self.widget.setPalette(palette)
1741 def TurnTextNormal(self):
1743 palette = QPalette()
1744 self.widget.setPalette(palette)
1747 def InvalidValue(self, value):
1750 self.error = self.label + " invalid value '" + value + "'"
1751 self.parent.ShowMessage(self.error)
1753 def Invalidate(self):
1754 self.validated = False
1756 def DoValidate(self, input_string):
1757 self.value = input_string.strip()
1760 self.validated = True
1762 self.TurnTextNormal()
1763 self.parent.ClearMessage()
1764 input_string = self.widget.text()
1765 if not len(input_string.strip()):
1768 self.DoValidate(input_string)
1771 if not self.validated:
1774 self.parent.ShowMessage(self.error)
1778 def IsNumber(self, value):
1783 return str(x) == value
1785 # Non-negative integer ranges dialog data item
1787 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1789 def __init__(self, glb, label, placeholder_text, column_name, parent):
1790 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1792 self.column_name = column_name
1794 def DoValidate(self, input_string):
1797 for value in [x.strip() for x in input_string.split(",")]:
1799 vrange = value.split("-")
1800 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1801 return self.InvalidValue(value)
1802 ranges.append(vrange)
1804 if not self.IsNumber(value):
1805 return self.InvalidValue(value)
1806 singles.append(value)
1807 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1809 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1810 self.value = " OR ".join(ranges)
1812 # Positive integer dialog data item
1814 class PositiveIntegerDataItem(LineEditDataItem):
1816 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1817 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1819 def DoValidate(self, input_string):
1820 if not self.IsNumber(input_string.strip()):
1821 return self.InvalidValue(input_string)
1822 value = int(input_string.strip())
1824 return self.InvalidValue(input_string)
1825 self.value = str(value)
1827 # Dialog data item converted and validated using a SQL table
1829 class SQLTableDataItem(LineEditDataItem):
1831 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1832 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1834 self.table_name = table_name
1835 self.match_column = match_column
1836 self.column_name1 = column_name1
1837 self.column_name2 = column_name2
1839 def ValueToIds(self, value):
1841 query = QSqlQuery(self.glb.db)
1842 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1843 ret = query.exec_(stmt)
1846 ids.append(str(query.value(0)))
1849 def DoValidate(self, input_string):
1851 for value in [x.strip() for x in input_string.split(",")]:
1852 ids = self.ValueToIds(value)
1856 return self.InvalidValue(value)
1857 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1858 if self.column_name2:
1859 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1861 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
1863 class SampleTimeRangesDataItem(LineEditDataItem):
1865 def __init__(self, glb, label, placeholder_text, column_name, parent):
1866 self.column_name = column_name
1870 self.last_time = 2 ** 64
1872 query = QSqlQuery(glb.db)
1873 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
1875 self.last_id = int(query.value(0))
1876 self.last_time = int(query.value(1))
1877 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
1879 self.first_time = int(query.value(0))
1880 if placeholder_text:
1881 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
1883 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1885 def IdBetween(self, query, lower_id, higher_id, order):
1886 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
1888 return True, int(query.value(0))
1892 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
1893 query = QSqlQuery(self.glb.db)
1895 next_id = int((lower_id + higher_id) / 2)
1896 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1897 if not query.next():
1898 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
1900 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
1902 return str(higher_id)
1904 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1905 next_time = int(query.value(0))
1907 if target_time > next_time:
1911 if higher_id <= lower_id + 1:
1912 return str(higher_id)
1914 if target_time >= next_time:
1918 if higher_id <= lower_id + 1:
1919 return str(lower_id)
1921 def ConvertRelativeTime(self, val):
1926 elif suffix == "us":
1928 elif suffix == "ns":
1932 val = val[:-2].strip()
1933 if not self.IsNumber(val):
1935 val = int(val) * mult
1937 val += self.first_time
1939 val += self.last_time
1942 def ConvertTimeRange(self, vrange):
1944 vrange[0] = str(self.first_time)
1946 vrange[1] = str(self.last_time)
1947 vrange[0] = self.ConvertRelativeTime(vrange[0])
1948 vrange[1] = self.ConvertRelativeTime(vrange[1])
1949 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1951 beg_range = max(int(vrange[0]), self.first_time)
1952 end_range = min(int(vrange[1]), self.last_time)
1953 if beg_range > self.last_time or end_range < self.first_time:
1955 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
1956 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
1959 def AddTimeRange(self, value, ranges):
1960 n = value.count("-")
1964 if value.split("-")[1].strip() == "":
1970 pos = findnth(value, "-", n)
1971 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
1972 if self.ConvertTimeRange(vrange):
1973 ranges.append(vrange)
1977 def DoValidate(self, input_string):
1979 for value in [x.strip() for x in input_string.split(",")]:
1980 if not self.AddTimeRange(value, ranges):
1981 return self.InvalidValue(value)
1982 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1983 self.value = " OR ".join(ranges)
1985 # Report Dialog Base
1987 class ReportDialogBase(QDialog):
1989 def __init__(self, glb, title, items, partial, parent=None):
1990 super(ReportDialogBase, self).__init__(parent)
1994 self.report_vars = ReportVars()
1996 self.setWindowTitle(title)
1997 self.setMinimumWidth(600)
1999 self.data_items = [x(glb, self) for x in items]
2001 self.partial = partial
2003 self.grid = QGridLayout()
2005 for row in xrange(len(self.data_items)):
2006 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
2007 self.grid.addWidget(self.data_items[row].widget, row, 1)
2009 self.status = QLabel()
2011 self.ok_button = QPushButton("Ok", self)
2012 self.ok_button.setDefault(True)
2013 self.ok_button.released.connect(self.Ok)
2014 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2016 self.cancel_button = QPushButton("Cancel", self)
2017 self.cancel_button.released.connect(self.reject)
2018 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2020 self.hbox = QHBoxLayout()
2021 #self.hbox.addStretch()
2022 self.hbox.addWidget(self.status)
2023 self.hbox.addWidget(self.ok_button)
2024 self.hbox.addWidget(self.cancel_button)
2026 self.vbox = QVBoxLayout()
2027 self.vbox.addLayout(self.grid)
2028 self.vbox.addLayout(self.hbox)
2030 self.setLayout(self.vbox);
2033 vars = self.report_vars
2034 for d in self.data_items:
2035 if d.id == "REPORTNAME":
2038 self.ShowMessage("Report name is required")
2040 for d in self.data_items:
2043 for d in self.data_items[1:]:
2045 vars.limit = d.value
2047 if len(vars.where_clause):
2048 vars.where_clause += " AND "
2049 vars.where_clause += d.value
2050 if len(vars.where_clause):
2052 vars.where_clause = " AND ( " + vars.where_clause + " ) "
2054 vars.where_clause = " WHERE " + vars.where_clause + " "
2057 def ShowMessage(self, msg):
2058 self.status.setText("<font color=#FF0000>" + msg)
2060 def ClearMessage(self):
2061 self.status.setText("")
2063 # Selected branch report creation dialog
2065 class SelectedBranchDialog(ReportDialogBase):
2067 def __init__(self, glb, parent=None):
2068 title = "Selected Branches"
2069 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2070 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2071 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2072 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2073 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2074 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2075 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2076 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2077 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2078 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2082 def GetEventList(db):
2084 query = QSqlQuery(db)
2085 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2087 events.append(query.value(0))
2090 # Is a table selectable
2092 def IsSelectable(db, table, sql = ""):
2093 query = QSqlQuery(db)
2095 QueryExec(query, "SELECT * FROM " + table + " " + sql + " LIMIT 1")
2100 # SQL table data model item
2102 class SQLTableItem():
2104 def __init__(self, row, data):
2108 def getData(self, column):
2109 return self.data[column]
2111 # SQL table data model
2113 class SQLTableModel(TableModel):
2115 progress = Signal(object)
2117 def __init__(self, glb, sql, column_headers, parent=None):
2118 super(SQLTableModel, self).__init__(parent)
2122 self.column_headers = column_headers
2123 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
2124 self.fetcher.done.connect(self.Update)
2125 self.fetcher.Fetch(glb_chunk_sz)
2127 def DisplayData(self, item, index):
2128 self.FetchIfNeeded(item.row)
2129 return item.getData(index.column())
2131 def AddSample(self, data):
2132 child = SQLTableItem(self.populated, data)
2133 self.child_items.append(child)
2136 def Update(self, fetched):
2139 self.progress.emit(0)
2140 child_count = self.child_count
2141 count = self.populated - child_count
2143 parent = QModelIndex()
2144 self.beginInsertRows(parent, child_count, child_count + count - 1)
2145 self.insertRows(child_count, count, parent)
2146 self.child_count += count
2147 self.endInsertRows()
2148 self.progress.emit(self.child_count)
2150 def FetchMoreRecords(self, count):
2151 current = self.child_count
2153 self.fetcher.Fetch(count)
2155 self.progress.emit(0)
2158 def HasMoreRecords(self):
2161 def columnCount(self, parent=None):
2162 return len(self.column_headers)
2164 def columnHeader(self, column):
2165 return self.column_headers[column]
2167 def SQLTableDataPrep(self, query, count):
2169 for i in xrange(count):
2170 data.append(query.value(i))
2173 # SQL automatic table data model
2175 class SQLAutoTableModel(SQLTableModel):
2177 def __init__(self, glb, table_name, parent=None):
2178 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2179 if table_name == "comm_threads_view":
2180 # For now, comm_threads_view has no id column
2181 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2183 query = QSqlQuery(glb.db)
2184 if glb.dbref.is_sqlite3:
2185 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2187 column_headers.append(query.value(1))
2188 if table_name == "sqlite_master":
2189 sql = "SELECT * FROM " + table_name
2191 if table_name[:19] == "information_schema.":
2192 sql = "SELECT * FROM " + table_name
2193 select_table_name = table_name[19:]
2194 schema = "information_schema"
2196 select_table_name = table_name
2198 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2200 column_headers.append(query.value(0))
2201 if pyside_version_1 and sys.version_info[0] == 3:
2202 if table_name == "samples_view":
2203 self.SQLTableDataPrep = self.samples_view_DataPrep
2204 if table_name == "samples":
2205 self.SQLTableDataPrep = self.samples_DataPrep
2206 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2208 def samples_view_DataPrep(self, query, count):
2210 data.append(query.value(0))
2211 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2212 data.append("{:>19}".format(query.value(1)))
2213 for i in xrange(2, count):
2214 data.append(query.value(i))
2217 def samples_DataPrep(self, query, count):
2220 data.append(query.value(i))
2221 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2222 data.append("{:>19}".format(query.value(9)))
2223 for i in xrange(10, count):
2224 data.append(query.value(i))
2227 # Base class for custom ResizeColumnsToContents
2229 class ResizeColumnsToContentsBase(QObject):
2231 def __init__(self, parent=None):
2232 super(ResizeColumnsToContentsBase, self).__init__(parent)
2234 def ResizeColumnToContents(self, column, n):
2235 # Using the view's resizeColumnToContents() here is extrememly slow
2236 # so implement a crude alternative
2237 font = self.view.font()
2238 metrics = QFontMetrics(font)
2240 for row in xrange(n):
2241 val = self.data_model.child_items[row].data[column]
2242 len = metrics.width(str(val) + "MM")
2243 max = len if len > max else max
2244 val = self.data_model.columnHeader(column)
2245 len = metrics.width(str(val) + "MM")
2246 max = len if len > max else max
2247 self.view.setColumnWidth(column, max)
2249 def ResizeColumnsToContents(self):
2250 n = min(self.data_model.child_count, 100)
2252 # No data yet, so connect a signal to notify when there is
2253 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2255 columns = self.data_model.columnCount()
2256 for i in xrange(columns):
2257 self.ResizeColumnToContents(i, n)
2259 def UpdateColumnWidths(self, *x):
2260 # This only needs to be done once, so disconnect the signal now
2261 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2262 self.ResizeColumnsToContents()
2266 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2268 def __init__(self, glb, table_name, parent=None):
2269 super(TableWindow, self).__init__(parent)
2271 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2273 self.model = QSortFilterProxyModel()
2274 self.model.setSourceModel(self.data_model)
2276 self.view = QTableView()
2277 self.view.setModel(self.model)
2278 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2279 self.view.verticalHeader().setVisible(False)
2280 self.view.sortByColumn(-1, Qt.AscendingOrder)
2281 self.view.setSortingEnabled(True)
2283 self.ResizeColumnsToContents()
2285 self.find_bar = FindBar(self, self, True)
2287 self.finder = ChildDataItemFinder(self.data_model)
2289 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2291 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2293 self.setWidget(self.vbox.Widget())
2295 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2297 def Find(self, value, direction, pattern, context):
2298 self.view.setFocus()
2299 self.find_bar.Busy()
2300 self.finder.Find(value, direction, pattern, context, self.FindDone)
2302 def FindDone(self, row):
2303 self.find_bar.Idle()
2305 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2307 self.find_bar.NotFound()
2311 def GetTableList(glb):
2313 query = QSqlQuery(glb.db)
2314 if glb.dbref.is_sqlite3:
2315 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2317 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2319 tables.append(query.value(0))
2320 if glb.dbref.is_sqlite3:
2321 tables.append("sqlite_master")
2323 tables.append("information_schema.tables")
2324 tables.append("information_schema.views")
2325 tables.append("information_schema.columns")
2328 # Top Calls data model
2330 class TopCallsModel(SQLTableModel):
2332 def __init__(self, glb, report_vars, parent=None):
2334 if not glb.dbref.is_sqlite3:
2337 if len(report_vars.limit):
2338 limit = " LIMIT " + report_vars.limit
2339 sql = ("SELECT comm, pid, tid, name,"
2341 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2344 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2346 " WHEN (calls.flags = 1) THEN 'no call'" + text +
2347 " WHEN (calls.flags = 2) THEN 'no return'" + text +
2348 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2352 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2353 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2354 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2355 " INNER JOIN comms ON calls.comm_id = comms.id"
2356 " INNER JOIN threads ON calls.thread_id = threads.id" +
2357 report_vars.where_clause +
2358 " ORDER BY elapsed_time DESC" +
2361 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2362 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2363 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2365 def columnAlignment(self, column):
2366 return self.alignment[column]
2368 # Top Calls report creation dialog
2370 class TopCallsDialog(ReportDialogBase):
2372 def __init__(self, glb, parent=None):
2373 title = "Top Calls by Elapsed Time"
2374 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2375 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2376 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2377 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2378 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2379 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2380 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2381 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2382 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2386 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2388 def __init__(self, glb, report_vars, parent=None):
2389 super(TopCallsWindow, self).__init__(parent)
2391 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2392 self.model = self.data_model
2394 self.view = QTableView()
2395 self.view.setModel(self.model)
2396 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2397 self.view.verticalHeader().setVisible(False)
2399 self.ResizeColumnsToContents()
2401 self.find_bar = FindBar(self, self, True)
2403 self.finder = ChildDataItemFinder(self.model)
2405 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2407 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2409 self.setWidget(self.vbox.Widget())
2411 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2413 def Find(self, value, direction, pattern, context):
2414 self.view.setFocus()
2415 self.find_bar.Busy()
2416 self.finder.Find(value, direction, pattern, context, self.FindDone)
2418 def FindDone(self, row):
2419 self.find_bar.Idle()
2421 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2423 self.find_bar.NotFound()
2427 def CreateAction(label, tip, callback, parent=None, shortcut=None):
2428 action = QAction(label, parent)
2429 if shortcut != None:
2430 action.setShortcuts(shortcut)
2431 action.setStatusTip(tip)
2432 action.triggered.connect(callback)
2435 # Typical application actions
2437 def CreateExitAction(app, parent=None):
2438 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2440 # Typical MDI actions
2442 def CreateCloseActiveWindowAction(mdi_area):
2443 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2445 def CreateCloseAllWindowsAction(mdi_area):
2446 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2448 def CreateTileWindowsAction(mdi_area):
2449 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2451 def CreateCascadeWindowsAction(mdi_area):
2452 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2454 def CreateNextWindowAction(mdi_area):
2455 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2457 def CreatePreviousWindowAction(mdi_area):
2458 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2460 # Typical MDI window menu
2464 def __init__(self, mdi_area, menu):
2465 self.mdi_area = mdi_area
2466 self.window_menu = menu.addMenu("&Windows")
2467 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2468 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2469 self.tile_windows = CreateTileWindowsAction(mdi_area)
2470 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2471 self.next_window = CreateNextWindowAction(mdi_area)
2472 self.previous_window = CreatePreviousWindowAction(mdi_area)
2473 self.window_menu.aboutToShow.connect(self.Update)
2476 self.window_menu.clear()
2477 sub_window_count = len(self.mdi_area.subWindowList())
2478 have_sub_windows = sub_window_count != 0
2479 self.close_active_window.setEnabled(have_sub_windows)
2480 self.close_all_windows.setEnabled(have_sub_windows)
2481 self.tile_windows.setEnabled(have_sub_windows)
2482 self.cascade_windows.setEnabled(have_sub_windows)
2483 self.next_window.setEnabled(have_sub_windows)
2484 self.previous_window.setEnabled(have_sub_windows)
2485 self.window_menu.addAction(self.close_active_window)
2486 self.window_menu.addAction(self.close_all_windows)
2487 self.window_menu.addSeparator()
2488 self.window_menu.addAction(self.tile_windows)
2489 self.window_menu.addAction(self.cascade_windows)
2490 self.window_menu.addSeparator()
2491 self.window_menu.addAction(self.next_window)
2492 self.window_menu.addAction(self.previous_window)
2493 if sub_window_count == 0:
2495 self.window_menu.addSeparator()
2497 for sub_window in self.mdi_area.subWindowList():
2498 label = str(nr) + " " + sub_window.name
2501 action = self.window_menu.addAction(label)
2502 action.setCheckable(True)
2503 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2504 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
2505 self.window_menu.addAction(action)
2508 def setActiveSubWindow(self, nr):
2509 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2524 <p class=c1><a href=#reports>1. Reports</a></p>
2525 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2526 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2527 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
2528 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2529 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2530 <p class=c1><a href=#tables>2. Tables</a></p>
2531 <h1 id=reports>1. Reports</h1>
2532 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2533 The result is a GUI window with a tree representing a context-sensitive
2534 call-graph. Expanding a couple of levels of the tree and adjusting column
2535 widths to suit will display something like:
2537 Call Graph: pt_example
2538 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
2541 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
2542 |- unknown unknown 1 13198 0.1 1 0.0
2543 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
2544 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
2545 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
2546 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
2547 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
2548 >- __libc_csu_init ls 1 10354 0.1 10 0.0
2549 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
2550 v- main ls 1 8182043 99.6 180254 99.9
2552 <h3>Points to note:</h3>
2554 <li>The top level is a command name (comm)</li>
2555 <li>The next level is a thread (pid:tid)</li>
2556 <li>Subsequent levels are functions</li>
2557 <li>'Count' is the number of calls</li>
2558 <li>'Time' is the elapsed time until the function returns</li>
2559 <li>Percentages are relative to the level above</li>
2560 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
2563 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2564 The pattern matching symbols are ? for any character and * for zero or more characters.
2565 <h2 id=calltree>1.2 Call Tree</h2>
2566 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2567 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2568 <h2 id=allbranches>1.3 All branches</h2>
2569 The All branches report displays all branches in chronological order.
2570 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2571 <h3>Disassembly</h3>
2572 Open a branch to display disassembly. This only works if:
2574 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2575 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2576 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2577 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2578 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2580 <h4 id=xed>Intel XED Setup</h4>
2581 To use Intel XED, libxed.so must be present. To build and install libxed.so:
2583 git clone https://github.com/intelxed/mbuild.git mbuild
2584 git clone https://github.com/intelxed/xed
2587 sudo ./mfile.py --prefix=/usr/local install
2591 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2592 Refer to Python documentation for the regular expression syntax.
2593 All columns are searched, but only currently fetched rows are searched.
2594 <h2 id=selectedbranches>1.4 Selected branches</h2>
2595 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2596 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2597 <h3>1.4.1 Time ranges</h3>
2598 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2599 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
2601 81073085947329-81073085958238 From 81073085947329 to 81073085958238
2602 100us-200us From 100us to 200us
2603 10ms- From 10ms to the end
2604 -100ns The first 100ns
2605 -10ms- The last 10ms
2607 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
2608 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
2609 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.
2610 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2611 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
2612 <h1 id=tables>2. Tables</h1>
2613 The Tables menu shows all tables and views in the database. Most tables have an associated view
2614 which displays the information in a more friendly way. Not all data for large tables is fetched
2615 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2616 but that can be slow for large tables.
2617 <p>There are also tables of database meta-information.
2618 For SQLite3 databases, the sqlite_master table is included.
2619 For PostgreSQL databases, information_schema.tables/views/columns are included.
2621 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2622 Refer to Python documentation for the regular expression syntax.
2623 All columns are searched, but only currently fetched rows are searched.
2624 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
2625 will go to the next/previous result in id order, instead of display order.
2630 class HelpWindow(QMdiSubWindow):
2632 def __init__(self, glb, parent=None):
2633 super(HelpWindow, self).__init__(parent)
2635 self.text = QTextBrowser()
2636 self.text.setHtml(glb_help_text)
2637 self.text.setReadOnly(True)
2638 self.text.setOpenExternalLinks(True)
2640 self.setWidget(self.text)
2642 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
2644 # Main window that only displays the help text
2646 class HelpOnlyWindow(QMainWindow):
2648 def __init__(self, parent=None):
2649 super(HelpOnlyWindow, self).__init__(parent)
2651 self.setMinimumSize(200, 100)
2652 self.resize(800, 600)
2653 self.setWindowTitle("Exported SQL Viewer Help")
2654 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
2656 self.text = QTextBrowser()
2657 self.text.setHtml(glb_help_text)
2658 self.text.setReadOnly(True)
2659 self.text.setOpenExternalLinks(True)
2661 self.setCentralWidget(self.text)
2665 def ResizeFont(widget, diff):
2666 font = widget.font()
2667 sz = font.pointSize()
2668 font.setPointSize(sz + diff)
2669 widget.setFont(font)
2671 def ShrinkFont(widget):
2672 ResizeFont(widget, -1)
2674 def EnlargeFont(widget):
2675 ResizeFont(widget, 1)
2677 # Unique name for sub-windows
2679 def NumberedWindowName(name, nr):
2681 name += " <" + str(nr) + ">"
2684 def UniqueSubWindowName(mdi_area, name):
2687 unique_name = NumberedWindowName(name, nr)
2689 for sub_window in mdi_area.subWindowList():
2690 if sub_window.name == unique_name:
2699 def AddSubWindow(mdi_area, sub_window, name):
2700 unique_name = UniqueSubWindowName(mdi_area, name)
2701 sub_window.setMinimumSize(200, 100)
2702 sub_window.resize(800, 600)
2703 sub_window.setWindowTitle(unique_name)
2704 sub_window.setAttribute(Qt.WA_DeleteOnClose)
2705 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
2706 sub_window.name = unique_name
2707 mdi_area.addSubWindow(sub_window)
2712 class MainWindow(QMainWindow):
2714 def __init__(self, glb, parent=None):
2715 super(MainWindow, self).__init__(parent)
2719 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
2720 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
2721 self.setMinimumSize(200, 100)
2723 self.mdi_area = QMdiArea()
2724 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2725 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2727 self.setCentralWidget(self.mdi_area)
2729 menu = self.menuBar()
2731 file_menu = menu.addMenu("&File")
2732 file_menu.addAction(CreateExitAction(glb.app, self))
2734 edit_menu = menu.addMenu("&Edit")
2735 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
2736 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
2737 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
2738 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
2740 reports_menu = menu.addMenu("&Reports")
2741 if IsSelectable(glb.db, "calls"):
2742 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
2744 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
2745 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
2747 self.EventMenu(GetEventList(glb.db), reports_menu)
2749 if IsSelectable(glb.db, "calls"):
2750 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
2752 self.TableMenu(GetTableList(glb), menu)
2754 self.window_menu = WindowMenu(self.mdi_area, menu)
2756 help_menu = menu.addMenu("&Help")
2757 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
2760 win = self.mdi_area.activeSubWindow()
2763 win.find_bar.Activate()
2767 def FetchMoreRecords(self):
2768 win = self.mdi_area.activeSubWindow()
2771 win.fetch_bar.Activate()
2775 def ShrinkFont(self):
2776 win = self.mdi_area.activeSubWindow()
2777 ShrinkFont(win.view)
2779 def EnlargeFont(self):
2780 win = self.mdi_area.activeSubWindow()
2781 EnlargeFont(win.view)
2783 def EventMenu(self, events, reports_menu):
2785 for event in events:
2786 event = event.split(":")[0]
2787 if event == "branches":
2788 branches_events += 1
2790 for event in events:
2792 event = event.split(":")[0]
2793 if event == "branches":
2794 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
2795 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self))
2796 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
2797 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))
2799 def TableMenu(self, tables, menu):
2800 table_menu = menu.addMenu("&Tables")
2801 for table in tables:
2802 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
2804 def NewCallGraph(self):
2805 CallGraphWindow(self.glb, self)
2807 def NewCallTree(self):
2808 CallTreeWindow(self.glb, self)
2810 def NewTopCalls(self):
2811 dialog = TopCallsDialog(self.glb, self)
2812 ret = dialog.exec_()
2814 TopCallsWindow(self.glb, dialog.report_vars, self)
2816 def NewBranchView(self, event_id):
2817 BranchWindow(self.glb, event_id, ReportVars(), self)
2819 def NewSelectedBranchView(self, event_id):
2820 dialog = SelectedBranchDialog(self.glb, self)
2821 ret = dialog.exec_()
2823 BranchWindow(self.glb, event_id, dialog.report_vars, self)
2825 def NewTableView(self, table_name):
2826 TableWindow(self.glb, table_name, self)
2829 HelpWindow(self.glb, self)
2833 class xed_state_t(Structure):
2840 class XEDInstruction():
2842 def __init__(self, libxed):
2843 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
2844 xedd_t = c_byte * 512
2845 self.xedd = xedd_t()
2846 self.xedp = addressof(self.xedd)
2847 libxed.xed_decoded_inst_zero(self.xedp)
2848 self.state = xed_state_t()
2849 self.statep = addressof(self.state)
2850 # Buffer for disassembled instruction text
2851 self.buffer = create_string_buffer(256)
2852 self.bufferp = addressof(self.buffer)
2858 self.libxed = CDLL("libxed.so")
2862 self.libxed = CDLL("/usr/local/lib/libxed.so")
2864 self.xed_tables_init = self.libxed.xed_tables_init
2865 self.xed_tables_init.restype = None
2866 self.xed_tables_init.argtypes = []
2868 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
2869 self.xed_decoded_inst_zero.restype = None
2870 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
2872 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
2873 self.xed_operand_values_set_mode.restype = None
2874 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
2876 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
2877 self.xed_decoded_inst_zero_keep_mode.restype = None
2878 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
2880 self.xed_decode = self.libxed.xed_decode
2881 self.xed_decode.restype = c_int
2882 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
2884 self.xed_format_context = self.libxed.xed_format_context
2885 self.xed_format_context.restype = c_uint
2886 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
2888 self.xed_tables_init()
2890 def Instruction(self):
2891 return XEDInstruction(self)
2893 def SetMode(self, inst, mode):
2895 inst.state.mode = 4 # 32-bit
2896 inst.state.width = 4 # 4 bytes
2898 inst.state.mode = 1 # 64-bit
2899 inst.state.width = 8 # 8 bytes
2900 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
2902 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
2903 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
2904 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
2907 # Use AT&T mode (2), alternative is Intel (3)
2908 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
2911 if sys.version_info[0] == 2:
2912 result = inst.buffer.value
2914 result = inst.buffer.value.decode()
2915 # Return instruction length and the disassembled instruction text
2916 # For now, assume the length is in byte 166
2917 return inst.xedd[166], result
2919 def TryOpen(file_name):
2921 return open(file_name, "rb")
2926 result = sizeof(c_void_p)
2933 if sys.version_info[0] == 2:
2934 eclass = ord(header[4])
2935 encoding = ord(header[5])
2936 version = ord(header[6])
2939 encoding = header[5]
2941 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
2942 result = True if eclass == 2 else False
2949 def __init__(self, dbref, db, dbname):
2952 self.dbname = dbname
2953 self.home_dir = os.path.expanduser("~")
2954 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
2955 if self.buildid_dir:
2956 self.buildid_dir += "/.build-id/"
2958 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
2960 self.mainwindow = None
2961 self.instances_to_shutdown_on_exit = weakref.WeakSet()
2963 self.disassembler = LibXED()
2964 self.have_disassembler = True
2966 self.have_disassembler = False
2968 def FileFromBuildId(self, build_id):
2969 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
2970 return TryOpen(file_name)
2972 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
2973 # Assume current machine i.e. no support for virtualization
2974 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
2975 file_name = os.getenv("PERF_KCORE")
2976 f = TryOpen(file_name) if file_name else None
2979 # For now, no special handling if long_name is /proc/kcore
2980 f = TryOpen(long_name)
2983 f = self.FileFromBuildId(build_id)
2988 def AddInstanceToShutdownOnExit(self, instance):
2989 self.instances_to_shutdown_on_exit.add(instance)
2991 # Shutdown any background processes or threads
2992 def ShutdownInstances(self):
2993 for x in self.instances_to_shutdown_on_exit:
2999 # Database reference
3003 def __init__(self, is_sqlite3, dbname):
3004 self.is_sqlite3 = is_sqlite3
3005 self.dbname = dbname
3007 def Open(self, connection_name):
3008 dbname = self.dbname
3010 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
3012 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
3013 opts = dbname.split()
3016 opt = opt.split("=")
3017 if opt[0] == "hostname":
3018 db.setHostName(opt[1])
3019 elif opt[0] == "port":
3020 db.setPort(int(opt[1]))
3021 elif opt[0] == "username":
3022 db.setUserName(opt[1])
3023 elif opt[0] == "password":
3024 db.setPassword(opt[1])
3025 elif opt[0] == "dbname":
3030 db.setDatabaseName(dbname)
3032 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
3038 if (len(sys.argv) < 2):
3039 printerr("Usage is: exported-sql-viewer.py {<database name> | --help-only}");
3040 raise Exception("Too few arguments")
3042 dbname = sys.argv[1]
3043 if dbname == "--help-only":
3044 app = QApplication(sys.argv)
3045 mainwindow = HelpOnlyWindow()
3052 f = open(dbname, "rb")
3053 if f.read(15) == b'SQLite format 3':
3059 dbref = DBRef(is_sqlite3, dbname)
3060 db, dbname = dbref.Open("main")
3061 glb = Glb(dbref, db, dbname)
3062 app = QApplication(sys.argv)
3064 mainwindow = MainWindow(glb)
3065 glb.mainwindow = mainwindow
3068 glb.ShutdownInstances()
3072 if __name__ == "__main__":