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