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