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