]> Git Repo - linux.git/blob - tools/perf/scripts/python/exported-sql-viewer.py
perf scripts python: exported-sql-viewer.py: Add IPC information to Call Graph Graph
[linux.git] / tools / perf / scripts / python / exported-sql-viewer.py
1 #!/usr/bin/env python
2 # SPDX-License-Identifier: GPL-2.0
3 # exported-sql-viewer.py: view data from sql database
4 # Copyright (c) 2014-2018, Intel Corporation.
5
6 # To use this script you will need to have exported data using either the
7 # export-to-sqlite.py or the export-to-postgresql.py script.  Refer to those
8 # scripts for details.
9 #
10 # Following on from the example in the export scripts, a
11 # call-graph can be displayed for the pt_example database like this:
12 #
13 #       python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14 #
15 # Note that for PostgreSQL, this script supports connecting to remote databases
16 # by setting hostname, port, username, password, and dbname e.g.
17 #
18 #       python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19 #
20 # The result is a GUI window with a tree representing a context-sensitive
21 # call-graph.  Expanding a couple of levels of the tree and adjusting column
22 # widths to suit will display something like:
23 #
24 #                                         Call Graph: pt_example
25 # Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
26 # v- ls
27 #     v- 2638:2638
28 #         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
29 #           |- unknown               unknown       1        13198     0.1              1              0.0
30 #           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
31 #           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
32 #           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
33 #              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
34 #              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
35 #              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
36 #              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
37 #              v- main               ls            1      8182043    99.6         180254             99.9
38 #
39 # Points to note:
40 #       The top level is a command name (comm)
41 #       The next level is a thread (pid:tid)
42 #       Subsequent levels are functions
43 #       'Count' is the number of calls
44 #       'Time' is the elapsed time until the function returns
45 #       Percentages are relative to the level above
46 #       'Branch Count' is the total number of branches for that function and all
47 #       functions that it calls
48
49 # There is also a "All branches" report, which displays branches and
50 # possibly disassembly.  However, presently, the only supported disassembler is
51 # Intel XED, and additionally the object code must be present in perf build ID
52 # cache. To use Intel XED, libxed.so must be present. To build and install
53 # libxed.so:
54 #            git clone https://github.com/intelxed/mbuild.git mbuild
55 #            git clone https://github.com/intelxed/xed
56 #            cd xed
57 #            ./mfile.py --share
58 #            sudo ./mfile.py --prefix=/usr/local install
59 #            sudo ldconfig
60 #
61 # Example report:
62 #
63 # Time           CPU  Command  PID    TID    Branch Type            In Tx  Branch
64 # 8107675239590  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65 #                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
66 # 8107675239899  2    ls       22011  22011  hardware interrupt     No         7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67 # 8107675241900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68 #                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
69 #                                                                              7fab593ea263 e8 c8 06 00 00                                  callq  0x7fab593ea930
70 # 8107675241900  2    ls       22011  22011  call                   No         7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71 #                                                                              7fab593ea930 55                                              pushq  %rbp
72 #                                                                              7fab593ea931 48 89 e5                                        mov %rsp, %rbp
73 #                                                                              7fab593ea934 41 57                                           pushq  %r15
74 #                                                                              7fab593ea936 41 56                                           pushq  %r14
75 #                                                                              7fab593ea938 41 55                                           pushq  %r13
76 #                                                                              7fab593ea93a 41 54                                           pushq  %r12
77 #                                                                              7fab593ea93c 53                                              pushq  %rbx
78 #                                                                              7fab593ea93d 48 89 fb                                        mov %rdi, %rbx
79 #                                                                              7fab593ea940 48 83 ec 68                                     sub $0x68, %rsp
80 #                                                                              7fab593ea944 0f 31                                           rdtsc
81 #                                                                              7fab593ea946 48 c1 e2 20                                     shl $0x20, %rdx
82 #                                                                              7fab593ea94a 89 c0                                           mov %eax, %eax
83 #                                                                              7fab593ea94c 48 09 c2                                        or %rax, %rdx
84 #                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
85 # 8107675242232  2    ls       22011  22011  hardware interrupt     No         7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86 # 8107675242900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87 #                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
88 #                                                                              7fab593ea956 48 89 15 3b 13 22 00                            movq  %rdx, 0x22133b(%rip)
89 # 8107675243232  2    ls       22011  22011  hardware interrupt     No         7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
91 from __future__ import print_function
92
93 import sys
94 import argparse
95 import weakref
96 import threading
97 import string
98 try:
99         # Python2
100         import cPickle as pickle
101         # size of pickled integer big enough for record size
102         glb_nsz = 8
103 except ImportError:
104         import pickle
105         glb_nsz = 16
106 import re
107 import os
108
109 pyside_version_1 = True
110 if not "--pyside-version-1" in sys.argv:
111         try:
112                 from PySide2.QtCore import *
113                 from PySide2.QtGui import *
114                 from PySide2.QtSql import *
115                 from PySide2.QtWidgets import *
116                 pyside_version_1 = False
117         except:
118                 pass
119
120 if pyside_version_1:
121         from PySide.QtCore import *
122         from PySide.QtGui import *
123         from PySide.QtSql import *
124
125 from decimal import *
126 from ctypes import *
127 from multiprocessing import Process, Array, Value, Event
128
129 # xrange is range in Python3
130 try:
131         xrange
132 except NameError:
133         xrange = range
134
135 def printerr(*args, **keyword_args):
136         print(*args, file=sys.stderr, **keyword_args)
137
138 # Data formatting helpers
139
140 def tohex(ip):
141         if ip < 0:
142                 ip += 1 << 64
143         return "%x" % ip
144
145 def offstr(offset):
146         if offset:
147                 return "+0x%x" % offset
148         return ""
149
150 def dsoname(name):
151         if name == "[kernel.kallsyms]":
152                 return "[kernel]"
153         return name
154
155 def findnth(s, sub, n, offs=0):
156         pos = s.find(sub)
157         if pos < 0:
158                 return pos
159         if n <= 1:
160                 return offs + pos
161         return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
162
163 # Percent to one decimal place
164
165 def PercentToOneDP(n, d):
166         if not d:
167                 return "0.0"
168         x = (n * Decimal(100)) / d
169         return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
170
171 # Helper for queries that must not fail
172
173 def QueryExec(query, stmt):
174         ret = query.exec_(stmt)
175         if not ret:
176                 raise Exception("Query failed: " + query.lastError().text())
177
178 # Background thread
179
180 class Thread(QThread):
181
182         done = Signal(object)
183
184         def __init__(self, task, param=None, parent=None):
185                 super(Thread, self).__init__(parent)
186                 self.task = task
187                 self.param = param
188
189         def run(self):
190                 while True:
191                         if self.param is None:
192                                 done, result = self.task()
193                         else:
194                                 done, result = self.task(self.param)
195                         self.done.emit(result)
196                         if done:
197                                 break
198
199 # Tree data model
200
201 class TreeModel(QAbstractItemModel):
202
203         def __init__(self, glb, params, parent=None):
204                 super(TreeModel, self).__init__(parent)
205                 self.glb = glb
206                 self.params = params
207                 self.root = self.GetRoot()
208                 self.last_row_read = 0
209
210         def Item(self, parent):
211                 if parent.isValid():
212                         return parent.internalPointer()
213                 else:
214                         return self.root
215
216         def rowCount(self, parent):
217                 result = self.Item(parent).childCount()
218                 if result < 0:
219                         result = 0
220                         self.dataChanged.emit(parent, parent)
221                 return result
222
223         def hasChildren(self, parent):
224                 return self.Item(parent).hasChildren()
225
226         def headerData(self, section, orientation, role):
227                 if role == Qt.TextAlignmentRole:
228                         return self.columnAlignment(section)
229                 if role != Qt.DisplayRole:
230                         return None
231                 if orientation != Qt.Horizontal:
232                         return None
233                 return self.columnHeader(section)
234
235         def parent(self, child):
236                 child_item = child.internalPointer()
237                 if child_item is self.root:
238                         return QModelIndex()
239                 parent_item = child_item.getParentItem()
240                 return self.createIndex(parent_item.getRow(), 0, parent_item)
241
242         def index(self, row, column, parent):
243                 child_item = self.Item(parent).getChildItem(row)
244                 return self.createIndex(row, column, child_item)
245
246         def DisplayData(self, item, index):
247                 return item.getData(index.column())
248
249         def FetchIfNeeded(self, row):
250                 if row > self.last_row_read:
251                         self.last_row_read = row
252                         if row + 10 >= self.root.child_count:
253                                 self.fetcher.Fetch(glb_chunk_sz)
254
255         def columnAlignment(self, column):
256                 return Qt.AlignLeft
257
258         def columnFont(self, column):
259                 return None
260
261         def data(self, index, role):
262                 if role == Qt.TextAlignmentRole:
263                         return self.columnAlignment(index.column())
264                 if role == Qt.FontRole:
265                         return self.columnFont(index.column())
266                 if role != Qt.DisplayRole:
267                         return None
268                 item = index.internalPointer()
269                 return self.DisplayData(item, index)
270
271 # Table data model
272
273 class TableModel(QAbstractTableModel):
274
275         def __init__(self, parent=None):
276                 super(TableModel, self).__init__(parent)
277                 self.child_count = 0
278                 self.child_items = []
279                 self.last_row_read = 0
280
281         def Item(self, parent):
282                 if parent.isValid():
283                         return parent.internalPointer()
284                 else:
285                         return self
286
287         def rowCount(self, parent):
288                 return self.child_count
289
290         def headerData(self, section, orientation, role):
291                 if role == Qt.TextAlignmentRole:
292                         return self.columnAlignment(section)
293                 if role != Qt.DisplayRole:
294                         return None
295                 if orientation != Qt.Horizontal:
296                         return None
297                 return self.columnHeader(section)
298
299         def index(self, row, column, parent):
300                 return self.createIndex(row, column, self.child_items[row])
301
302         def DisplayData(self, item, index):
303                 return item.getData(index.column())
304
305         def FetchIfNeeded(self, row):
306                 if row > self.last_row_read:
307                         self.last_row_read = row
308                         if row + 10 >= self.child_count:
309                                 self.fetcher.Fetch(glb_chunk_sz)
310
311         def columnAlignment(self, column):
312                 return Qt.AlignLeft
313
314         def columnFont(self, column):
315                 return None
316
317         def data(self, index, role):
318                 if role == Qt.TextAlignmentRole:
319                         return self.columnAlignment(index.column())
320                 if role == Qt.FontRole:
321                         return self.columnFont(index.column())
322                 if role != Qt.DisplayRole:
323                         return None
324                 item = index.internalPointer()
325                 return self.DisplayData(item, index)
326
327 # Model cache
328
329 model_cache = weakref.WeakValueDictionary()
330 model_cache_lock = threading.Lock()
331
332 def LookupCreateModel(model_name, create_fn):
333         model_cache_lock.acquire()
334         try:
335                 model = model_cache[model_name]
336         except:
337                 model = None
338         if model is None:
339                 model = create_fn()
340                 model_cache[model_name] = model
341         model_cache_lock.release()
342         return model
343
344 # Find bar
345
346 class FindBar():
347
348         def __init__(self, parent, finder, is_reg_expr=False):
349                 self.finder = finder
350                 self.context = []
351                 self.last_value = None
352                 self.last_pattern = None
353
354                 label = QLabel("Find:")
355                 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
356
357                 self.textbox = QComboBox()
358                 self.textbox.setEditable(True)
359                 self.textbox.currentIndexChanged.connect(self.ValueChanged)
360
361                 self.progress = QProgressBar()
362                 self.progress.setRange(0, 0)
363                 self.progress.hide()
364
365                 if is_reg_expr:
366                         self.pattern = QCheckBox("Regular Expression")
367                 else:
368                         self.pattern = QCheckBox("Pattern")
369                 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
370
371                 self.next_button = QToolButton()
372                 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
373                 self.next_button.released.connect(lambda: self.NextPrev(1))
374
375                 self.prev_button = QToolButton()
376                 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
377                 self.prev_button.released.connect(lambda: self.NextPrev(-1))
378
379                 self.close_button = QToolButton()
380                 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
381                 self.close_button.released.connect(self.Deactivate)
382
383                 self.hbox = QHBoxLayout()
384                 self.hbox.setContentsMargins(0, 0, 0, 0)
385
386                 self.hbox.addWidget(label)
387                 self.hbox.addWidget(self.textbox)
388                 self.hbox.addWidget(self.progress)
389                 self.hbox.addWidget(self.pattern)
390                 self.hbox.addWidget(self.next_button)
391                 self.hbox.addWidget(self.prev_button)
392                 self.hbox.addWidget(self.close_button)
393
394                 self.bar = QWidget()
395                 self.bar.setLayout(self.hbox);
396                 self.bar.hide()
397
398         def Widget(self):
399                 return self.bar
400
401         def Activate(self):
402                 self.bar.show()
403                 self.textbox.setFocus()
404
405         def Deactivate(self):
406                 self.bar.hide()
407
408         def Busy(self):
409                 self.textbox.setEnabled(False)
410                 self.pattern.hide()
411                 self.next_button.hide()
412                 self.prev_button.hide()
413                 self.progress.show()
414
415         def Idle(self):
416                 self.textbox.setEnabled(True)
417                 self.progress.hide()
418                 self.pattern.show()
419                 self.next_button.show()
420                 self.prev_button.show()
421
422         def Find(self, direction):
423                 value = self.textbox.currentText()
424                 pattern = self.pattern.isChecked()
425                 self.last_value = value
426                 self.last_pattern = pattern
427                 self.finder.Find(value, direction, pattern, self.context)
428
429         def ValueChanged(self):
430                 value = self.textbox.currentText()
431                 pattern = self.pattern.isChecked()
432                 index = self.textbox.currentIndex()
433                 data = self.textbox.itemData(index)
434                 # Store the pattern in the combo box to keep it with the text value
435                 if data == None:
436                         self.textbox.setItemData(index, pattern)
437                 else:
438                         self.pattern.setChecked(data)
439                 self.Find(0)
440
441         def NextPrev(self, direction):
442                 value = self.textbox.currentText()
443                 pattern = self.pattern.isChecked()
444                 if value != self.last_value:
445                         index = self.textbox.findText(value)
446                         # Allow for a button press before the value has been added to the combo box
447                         if index < 0:
448                                 index = self.textbox.count()
449                                 self.textbox.addItem(value, pattern)
450                                 self.textbox.setCurrentIndex(index)
451                                 return
452                         else:
453                                 self.textbox.setItemData(index, pattern)
454                 elif pattern != self.last_pattern:
455                         # Keep the pattern recorded in the combo box up to date
456                         index = self.textbox.currentIndex()
457                         self.textbox.setItemData(index, pattern)
458                 self.Find(direction)
459
460         def NotFound(self):
461                 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
462
463 # Context-sensitive call graph data model item base
464
465 class CallGraphLevelItemBase(object):
466
467         def __init__(self, glb, params, row, parent_item):
468                 self.glb = glb
469                 self.params = params
470                 self.row = row
471                 self.parent_item = parent_item
472                 self.query_done = False;
473                 self.child_count = 0
474                 self.child_items = []
475                 if parent_item:
476                         self.level = parent_item.level + 1
477                 else:
478                         self.level = 0
479
480         def getChildItem(self, row):
481                 return self.child_items[row]
482
483         def getParentItem(self):
484                 return self.parent_item
485
486         def getRow(self):
487                 return self.row
488
489         def childCount(self):
490                 if not self.query_done:
491                         self.Select()
492                         if not self.child_count:
493                                 return -1
494                 return self.child_count
495
496         def hasChildren(self):
497                 if not self.query_done:
498                         return True
499                 return self.child_count > 0
500
501         def getData(self, column):
502                 return self.data[column]
503
504 # Context-sensitive call graph data model level 2+ item base
505
506 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
507
508         def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
509                 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
510                 self.comm_id = comm_id
511                 self.thread_id = thread_id
512                 self.call_path_id = call_path_id
513                 self.insn_cnt = insn_cnt
514                 self.cyc_cnt = cyc_cnt
515                 self.branch_count = branch_count
516                 self.time = time
517
518         def Select(self):
519                 self.query_done = True;
520                 query = QSqlQuery(self.glb.db)
521                 if self.params.have_ipc:
522                         ipc_str = ", SUM(insn_count), SUM(cyc_count)"
523                 else:
524                         ipc_str = ""
525                 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
526                                         " FROM calls"
527                                         " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
528                                         " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
529                                         " INNER JOIN dsos ON symbols.dso_id = dsos.id"
530                                         " WHERE parent_call_path_id = " + str(self.call_path_id) +
531                                         " AND comm_id = " + str(self.comm_id) +
532                                         " AND thread_id = " + str(self.thread_id) +
533                                         " GROUP BY call_path_id, name, short_name"
534                                         " ORDER BY call_path_id")
535                 while query.next():
536                         if self.params.have_ipc:
537                                 insn_cnt = int(query.value(5))
538                                 cyc_cnt = int(query.value(6))
539                                 branch_count = int(query.value(7))
540                         else:
541                                 insn_cnt = 0
542                                 cyc_cnt = 0
543                                 branch_count = int(query.value(5))
544                         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)
545                         self.child_items.append(child_item)
546                         self.child_count += 1
547
548 # Context-sensitive call graph data model level three item
549
550 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
551
552         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):
553                 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
554                 dso = dsoname(dso)
555                 if self.params.have_ipc:
556                         insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
557                         cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
558                         br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
559                         ipc = CalcIPC(cyc_cnt, insn_cnt)
560                         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 ]
561                 else:
562                         self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
563                 self.dbid = call_path_id
564
565 # Context-sensitive call graph data model level two item
566
567 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
568
569         def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
570                 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
571                 if self.params.have_ipc:
572                         self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
573                 else:
574                         self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
575                 self.dbid = thread_id
576
577         def Select(self):
578                 super(CallGraphLevelTwoItem, self).Select()
579                 for child_item in self.child_items:
580                         self.time += child_item.time
581                         self.insn_cnt += child_item.insn_cnt
582                         self.cyc_cnt += child_item.cyc_cnt
583                         self.branch_count += child_item.branch_count
584                 for child_item in self.child_items:
585                         child_item.data[4] = PercentToOneDP(child_item.time, self.time)
586                         if self.params.have_ipc:
587                                 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
588                                 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
589                                 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
590                         else:
591                                 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
592
593 # Context-sensitive call graph data model level one item
594
595 class CallGraphLevelOneItem(CallGraphLevelItemBase):
596
597         def __init__(self, glb, params, row, comm_id, comm, parent_item):
598                 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
599                 if self.params.have_ipc:
600                         self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
601                 else:
602                         self.data = [comm, "", "", "", "", "", ""]
603                 self.dbid = comm_id
604
605         def Select(self):
606                 self.query_done = True;
607                 query = QSqlQuery(self.glb.db)
608                 QueryExec(query, "SELECT thread_id, pid, tid"
609                                         " FROM comm_threads"
610                                         " INNER JOIN threads ON thread_id = threads.id"
611                                         " WHERE comm_id = " + str(self.dbid))
612                 while query.next():
613                         child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
614                         self.child_items.append(child_item)
615                         self.child_count += 1
616
617 # Context-sensitive call graph data model root item
618
619 class CallGraphRootItem(CallGraphLevelItemBase):
620
621         def __init__(self, glb, params):
622                 super(CallGraphRootItem, self).__init__(glb, params, 0, None)
623                 self.dbid = 0
624                 self.query_done = True;
625                 query = QSqlQuery(glb.db)
626                 QueryExec(query, "SELECT id, comm FROM comms")
627                 while query.next():
628                         if not query.value(0):
629                                 continue
630                         child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
631                         self.child_items.append(child_item)
632                         self.child_count += 1
633
634 # Call graph model parameters
635
636 class CallGraphModelParams():
637
638         def __init__(self, glb, parent=None):
639                 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
640
641 # Context-sensitive call graph data model base
642
643 class CallGraphModelBase(TreeModel):
644
645         def __init__(self, glb, parent=None):
646                 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
647
648         def FindSelect(self, value, pattern, query):
649                 if pattern:
650                         # postgresql and sqlite pattern patching differences:
651                         #   postgresql LIKE is case sensitive but sqlite LIKE is not
652                         #   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
653                         #   postgresql supports ILIKE which is case insensitive
654                         #   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
655                         if not self.glb.dbref.is_sqlite3:
656                                 # Escape % and _
657                                 s = value.replace("%", "\%")
658                                 s = s.replace("_", "\_")
659                                 # Translate * and ? into SQL LIKE pattern characters % and _
660                                 trans = string.maketrans("*?", "%_")
661                                 match = " LIKE '" + str(s).translate(trans) + "'"
662                         else:
663                                 match = " GLOB '" + str(value) + "'"
664                 else:
665                         match = " = '" + str(value) + "'"
666                 self.DoFindSelect(query, match)
667
668         def Found(self, query, found):
669                 if found:
670                         return self.FindPath(query)
671                 return []
672
673         def FindValue(self, value, pattern, query, last_value, last_pattern):
674                 if last_value == value and pattern == last_pattern:
675                         found = query.first()
676                 else:
677                         self.FindSelect(value, pattern, query)
678                         found = query.next()
679                 return self.Found(query, found)
680
681         def FindNext(self, query):
682                 found = query.next()
683                 if not found:
684                         found = query.first()
685                 return self.Found(query, found)
686
687         def FindPrev(self, query):
688                 found = query.previous()
689                 if not found:
690                         found = query.last()
691                 return self.Found(query, found)
692
693         def FindThread(self, c):
694                 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
695                         ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
696                 elif c.direction > 0:
697                         ids = self.FindNext(c.query)
698                 else:
699                         ids = self.FindPrev(c.query)
700                 return (True, ids)
701
702         def Find(self, value, direction, pattern, context, callback):
703                 class Context():
704                         def __init__(self, *x):
705                                 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
706                         def Update(self, *x):
707                                 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
708                 if len(context):
709                         context[0].Update(value, direction, pattern)
710                 else:
711                         context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
712                 # Use a thread so the UI is not blocked during the SELECT
713                 thread = Thread(self.FindThread, context[0])
714                 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
715                 thread.start()
716
717         def FindDone(self, thread, callback, ids):
718                 callback(ids)
719
720 # Context-sensitive call graph data model
721
722 class CallGraphModel(CallGraphModelBase):
723
724         def __init__(self, glb, parent=None):
725                 super(CallGraphModel, self).__init__(glb, parent)
726
727         def GetRoot(self):
728                 return CallGraphRootItem(self.glb, self.params)
729
730         def columnCount(self, parent=None):
731                 if self.params.have_ipc:
732                         return 12
733                 else:
734                         return 7
735
736         def columnHeader(self, column):
737                 if self.params.have_ipc:
738                         headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
739                 else:
740                         headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
741                 return headers[column]
742
743         def columnAlignment(self, column):
744                 if self.params.have_ipc:
745                         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 ]
746                 else:
747                         alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
748                 return alignment[column]
749
750         def DoFindSelect(self, query, match):
751                 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
752                                                 " FROM calls"
753                                                 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
754                                                 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
755                                                 " WHERE symbols.name" + match +
756                                                 " GROUP BY comm_id, thread_id, call_path_id"
757                                                 " ORDER BY comm_id, thread_id, call_path_id")
758
759         def FindPath(self, query):
760                 # Turn the query result into a list of ids that the tree view can walk
761                 # to open the tree at the right place.
762                 ids = []
763                 parent_id = query.value(0)
764                 while parent_id:
765                         ids.insert(0, parent_id)
766                         q2 = QSqlQuery(self.glb.db)
767                         QueryExec(q2, "SELECT parent_id"
768                                         " FROM call_paths"
769                                         " WHERE id = " + str(parent_id))
770                         if not q2.next():
771                                 break
772                         parent_id = q2.value(0)
773                 # The call path root is not used
774                 if ids[0] == 1:
775                         del ids[0]
776                 ids.insert(0, query.value(2))
777                 ids.insert(0, query.value(1))
778                 return ids
779
780 # Call tree data model level 2+ item base
781
782 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
783
784         def __init__(self, glb, params, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
785                 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
786                 self.comm_id = comm_id
787                 self.thread_id = thread_id
788                 self.calls_id = calls_id
789                 self.branch_count = branch_count
790                 self.time = time
791
792         def Select(self):
793                 self.query_done = True;
794                 if self.calls_id == 0:
795                         comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
796                 else:
797                         comm_thread = ""
798                 query = QSqlQuery(self.glb.db)
799                 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
800                                         " FROM calls"
801                                         " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
802                                         " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
803                                         " INNER JOIN dsos ON symbols.dso_id = dsos.id"
804                                         " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
805                                         " ORDER BY call_time, calls.id")
806                 while query.next():
807                         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)), int(query.value(5)), self)
808                         self.child_items.append(child_item)
809                         self.child_count += 1
810
811 # Call tree data model level three item
812
813 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
814
815         def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
816                 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
817                 dso = dsoname(dso)
818                 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
819                 self.dbid = calls_id
820
821 # Call tree data model level two item
822
823 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
824
825         def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
826                 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, parent_item)
827                 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
828                 self.dbid = thread_id
829
830         def Select(self):
831                 super(CallTreeLevelTwoItem, self).Select()
832                 for child_item in self.child_items:
833                         self.time += child_item.time
834                         self.branch_count += child_item.branch_count
835                 for child_item in self.child_items:
836                         child_item.data[4] = PercentToOneDP(child_item.time, self.time)
837                         child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
838
839 # Call tree data model level one item
840
841 class CallTreeLevelOneItem(CallGraphLevelItemBase):
842
843         def __init__(self, glb, params, row, comm_id, comm, parent_item):
844                 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
845                 self.data = [comm, "", "", "", "", "", ""]
846                 self.dbid = comm_id
847
848         def Select(self):
849                 self.query_done = True;
850                 query = QSqlQuery(self.glb.db)
851                 QueryExec(query, "SELECT thread_id, pid, tid"
852                                         " FROM comm_threads"
853                                         " INNER JOIN threads ON thread_id = threads.id"
854                                         " WHERE comm_id = " + str(self.dbid))
855                 while query.next():
856                         child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
857                         self.child_items.append(child_item)
858                         self.child_count += 1
859
860 # Call tree data model root item
861
862 class CallTreeRootItem(CallGraphLevelItemBase):
863
864         def __init__(self, glb, params):
865                 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
866                 self.dbid = 0
867                 self.query_done = True;
868                 query = QSqlQuery(glb.db)
869                 QueryExec(query, "SELECT id, comm FROM comms")
870                 while query.next():
871                         if not query.value(0):
872                                 continue
873                         child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
874                         self.child_items.append(child_item)
875                         self.child_count += 1
876
877 # Call Tree data model
878
879 class CallTreeModel(CallGraphModelBase):
880
881         def __init__(self, glb, parent=None):
882                 super(CallTreeModel, self).__init__(glb, parent)
883
884         def GetRoot(self):
885                 return CallTreeRootItem(self.glb, self.params)
886
887         def columnCount(self, parent=None):
888                 return 7
889
890         def columnHeader(self, column):
891                 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
892                 return headers[column]
893
894         def columnAlignment(self, column):
895                 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
896                 return alignment[column]
897
898         def DoFindSelect(self, query, match):
899                 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
900                                                 " FROM calls"
901                                                 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
902                                                 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
903                                                 " WHERE symbols.name" + match +
904                                                 " ORDER BY comm_id, thread_id, call_time, calls.id")
905
906         def FindPath(self, query):
907                 # Turn the query result into a list of ids that the tree view can walk
908                 # to open the tree at the right place.
909                 ids = []
910                 parent_id = query.value(0)
911                 while parent_id:
912                         ids.insert(0, parent_id)
913                         q2 = QSqlQuery(self.glb.db)
914                         QueryExec(q2, "SELECT parent_id"
915                                         " FROM calls"
916                                         " WHERE id = " + str(parent_id))
917                         if not q2.next():
918                                 break
919                         parent_id = q2.value(0)
920                 ids.insert(0, query.value(2))
921                 ids.insert(0, query.value(1))
922                 return ids
923
924 # Vertical widget layout
925
926 class VBox():
927
928         def __init__(self, w1, w2, w3=None):
929                 self.vbox = QWidget()
930                 self.vbox.setLayout(QVBoxLayout());
931
932                 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
933
934                 self.vbox.layout().addWidget(w1)
935                 self.vbox.layout().addWidget(w2)
936                 if w3:
937                         self.vbox.layout().addWidget(w3)
938
939         def Widget(self):
940                 return self.vbox
941
942 # Tree window base
943
944 class TreeWindowBase(QMdiSubWindow):
945
946         def __init__(self, parent=None):
947                 super(TreeWindowBase, self).__init__(parent)
948
949                 self.model = None
950                 self.find_bar = None
951
952                 self.view = QTreeView()
953                 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
954                 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
955
956                 self.context_menu = TreeContextMenu(self.view)
957
958         def DisplayFound(self, ids):
959                 if not len(ids):
960                         return False
961                 parent = QModelIndex()
962                 for dbid in ids:
963                         found = False
964                         n = self.model.rowCount(parent)
965                         for row in xrange(n):
966                                 child = self.model.index(row, 0, parent)
967                                 if child.internalPointer().dbid == dbid:
968                                         found = True
969                                         self.view.setCurrentIndex(child)
970                                         parent = child
971                                         break
972                         if not found:
973                                 break
974                 return found
975
976         def Find(self, value, direction, pattern, context):
977                 self.view.setFocus()
978                 self.find_bar.Busy()
979                 self.model.Find(value, direction, pattern, context, self.FindDone)
980
981         def FindDone(self, ids):
982                 found = True
983                 if not self.DisplayFound(ids):
984                         found = False
985                 self.find_bar.Idle()
986                 if not found:
987                         self.find_bar.NotFound()
988
989
990 # Context-sensitive call graph window
991
992 class CallGraphWindow(TreeWindowBase):
993
994         def __init__(self, glb, parent=None):
995                 super(CallGraphWindow, self).__init__(parent)
996
997                 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
998
999                 self.view.setModel(self.model)
1000
1001                 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1002                         self.view.setColumnWidth(c, w)
1003
1004                 self.find_bar = FindBar(self, self)
1005
1006                 self.vbox = VBox(self.view, self.find_bar.Widget())
1007
1008                 self.setWidget(self.vbox.Widget())
1009
1010                 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1011
1012 # Call tree window
1013
1014 class CallTreeWindow(TreeWindowBase):
1015
1016         def __init__(self, glb, parent=None):
1017                 super(CallTreeWindow, self).__init__(parent)
1018
1019                 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1020
1021                 self.view.setModel(self.model)
1022
1023                 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1024                         self.view.setColumnWidth(c, w)
1025
1026                 self.find_bar = FindBar(self, self)
1027
1028                 self.vbox = VBox(self.view, self.find_bar.Widget())
1029
1030                 self.setWidget(self.vbox.Widget())
1031
1032                 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1033
1034 # Child data item  finder
1035
1036 class ChildDataItemFinder():
1037
1038         def __init__(self, root):
1039                 self.root = root
1040                 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
1041                 self.rows = []
1042                 self.pos = 0
1043
1044         def FindSelect(self):
1045                 self.rows = []
1046                 if self.pattern:
1047                         pattern = re.compile(self.value)
1048                         for child in self.root.child_items:
1049                                 for column_data in child.data:
1050                                         if re.search(pattern, str(column_data)) is not None:
1051                                                 self.rows.append(child.row)
1052                                                 break
1053                 else:
1054                         for child in self.root.child_items:
1055                                 for column_data in child.data:
1056                                         if self.value in str(column_data):
1057                                                 self.rows.append(child.row)
1058                                                 break
1059
1060         def FindValue(self):
1061                 self.pos = 0
1062                 if self.last_value != self.value or self.pattern != self.last_pattern:
1063                         self.FindSelect()
1064                 if not len(self.rows):
1065                         return -1
1066                 return self.rows[self.pos]
1067
1068         def FindThread(self):
1069                 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
1070                         row = self.FindValue()
1071                 elif len(self.rows):
1072                         if self.direction > 0:
1073                                 self.pos += 1
1074                                 if self.pos >= len(self.rows):
1075                                         self.pos = 0
1076                         else:
1077                                 self.pos -= 1
1078                                 if self.pos < 0:
1079                                         self.pos = len(self.rows) - 1
1080                         row = self.rows[self.pos]
1081                 else:
1082                         row = -1
1083                 return (True, row)
1084
1085         def Find(self, value, direction, pattern, context, callback):
1086                 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1087                 # Use a thread so the UI is not blocked
1088                 thread = Thread(self.FindThread)
1089                 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1090                 thread.start()
1091
1092         def FindDone(self, thread, callback, row):
1093                 callback(row)
1094
1095 # Number of database records to fetch in one go
1096
1097 glb_chunk_sz = 10000
1098
1099 # Background process for SQL data fetcher
1100
1101 class SQLFetcherProcess():
1102
1103         def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1104                 # Need a unique connection name
1105                 conn_name = "SQLFetcher" + str(os.getpid())
1106                 self.db, dbname = dbref.Open(conn_name)
1107                 self.sql = sql
1108                 self.buffer = buffer
1109                 self.head = head
1110                 self.tail = tail
1111                 self.fetch_count = fetch_count
1112                 self.fetching_done = fetching_done
1113                 self.process_target = process_target
1114                 self.wait_event = wait_event
1115                 self.fetched_event = fetched_event
1116                 self.prep = prep
1117                 self.query = QSqlQuery(self.db)
1118                 self.query_limit = 0 if "$$last_id$$" in sql else 2
1119                 self.last_id = -1
1120                 self.fetched = 0
1121                 self.more = True
1122                 self.local_head = self.head.value
1123                 self.local_tail = self.tail.value
1124
1125         def Select(self):
1126                 if self.query_limit:
1127                         if self.query_limit == 1:
1128                                 return
1129                         self.query_limit -= 1
1130                 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1131                 QueryExec(self.query, stmt)
1132
1133         def Next(self):
1134                 if not self.query.next():
1135                         self.Select()
1136                         if not self.query.next():
1137                                 return None
1138                 self.last_id = self.query.value(0)
1139                 return self.prep(self.query)
1140
1141         def WaitForTarget(self):
1142                 while True:
1143                         self.wait_event.clear()
1144                         target = self.process_target.value
1145                         if target > self.fetched or target < 0:
1146                                 break
1147                         self.wait_event.wait()
1148                 return target
1149
1150         def HasSpace(self, sz):
1151                 if self.local_tail <= self.local_head:
1152                         space = len(self.buffer) - self.local_head
1153                         if space > sz:
1154                                 return True
1155                         if space >= glb_nsz:
1156                                 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1157                                 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
1158                                 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1159                         self.local_head = 0
1160                 if self.local_tail - self.local_head > sz:
1161                         return True
1162                 return False
1163
1164         def WaitForSpace(self, sz):
1165                 if self.HasSpace(sz):
1166                         return
1167                 while True:
1168                         self.wait_event.clear()
1169                         self.local_tail = self.tail.value
1170                         if self.HasSpace(sz):
1171                                 return
1172                         self.wait_event.wait()
1173
1174         def AddToBuffer(self, obj):
1175                 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
1176                 n = len(d)
1177                 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
1178                 sz = n + glb_nsz
1179                 self.WaitForSpace(sz)
1180                 pos = self.local_head
1181                 self.buffer[pos : pos + len(nd)] = nd
1182                 self.buffer[pos + glb_nsz : pos + sz] = d
1183                 self.local_head += sz
1184
1185         def FetchBatch(self, batch_size):
1186                 fetched = 0
1187                 while batch_size > fetched:
1188                         obj = self.Next()
1189                         if obj is None:
1190                                 self.more = False
1191                                 break
1192                         self.AddToBuffer(obj)
1193                         fetched += 1
1194                 if fetched:
1195                         self.fetched += fetched
1196                         with self.fetch_count.get_lock():
1197                                 self.fetch_count.value += fetched
1198                         self.head.value = self.local_head
1199                         self.fetched_event.set()
1200
1201         def Run(self):
1202                 while self.more:
1203                         target = self.WaitForTarget()
1204                         if target < 0:
1205                                 break
1206                         batch_size = min(glb_chunk_sz, target - self.fetched)
1207                         self.FetchBatch(batch_size)
1208                 self.fetching_done.value = True
1209                 self.fetched_event.set()
1210
1211 def SQLFetcherFn(*x):
1212         process = SQLFetcherProcess(*x)
1213         process.Run()
1214
1215 # SQL data fetcher
1216
1217 class SQLFetcher(QObject):
1218
1219         done = Signal(object)
1220
1221         def __init__(self, glb, sql, prep, process_data, parent=None):
1222                 super(SQLFetcher, self).__init__(parent)
1223                 self.process_data = process_data
1224                 self.more = True
1225                 self.target = 0
1226                 self.last_target = 0
1227                 self.fetched = 0
1228                 self.buffer_size = 16 * 1024 * 1024
1229                 self.buffer = Array(c_char, self.buffer_size, lock=False)
1230                 self.head = Value(c_longlong)
1231                 self.tail = Value(c_longlong)
1232                 self.local_tail = 0
1233                 self.fetch_count = Value(c_longlong)
1234                 self.fetching_done = Value(c_bool)
1235                 self.last_count = 0
1236                 self.process_target = Value(c_longlong)
1237                 self.wait_event = Event()
1238                 self.fetched_event = Event()
1239                 glb.AddInstanceToShutdownOnExit(self)
1240                 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))
1241                 self.process.start()
1242                 self.thread = Thread(self.Thread)
1243                 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1244                 self.thread.start()
1245
1246         def Shutdown(self):
1247                 # Tell the thread and process to exit
1248                 self.process_target.value = -1
1249                 self.wait_event.set()
1250                 self.more = False
1251                 self.fetching_done.value = True
1252                 self.fetched_event.set()
1253
1254         def Thread(self):
1255                 if not self.more:
1256                         return True, 0
1257                 while True:
1258                         self.fetched_event.clear()
1259                         fetch_count = self.fetch_count.value
1260                         if fetch_count != self.last_count:
1261                                 break
1262                         if self.fetching_done.value:
1263                                 self.more = False
1264                                 return True, 0
1265                         self.fetched_event.wait()
1266                 count = fetch_count - self.last_count
1267                 self.last_count = fetch_count
1268                 self.fetched += count
1269                 return False, count
1270
1271         def Fetch(self, nr):
1272                 if not self.more:
1273                         # -1 inidcates there are no more
1274                         return -1
1275                 result = self.fetched
1276                 extra = result + nr - self.target
1277                 if extra > 0:
1278                         self.target += extra
1279                         # process_target < 0 indicates shutting down
1280                         if self.process_target.value >= 0:
1281                                 self.process_target.value = self.target
1282                         self.wait_event.set()
1283                 return result
1284
1285         def RemoveFromBuffer(self):
1286                 pos = self.local_tail
1287                 if len(self.buffer) - pos < glb_nsz:
1288                         pos = 0
1289                 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
1290                 if n == 0:
1291                         pos = 0
1292                         n = pickle.loads(self.buffer[0 : glb_nsz])
1293                 pos += glb_nsz
1294                 obj = pickle.loads(self.buffer[pos : pos + n])
1295                 self.local_tail = pos + n
1296                 return obj
1297
1298         def ProcessData(self, count):
1299                 for i in xrange(count):
1300                         obj = self.RemoveFromBuffer()
1301                         self.process_data(obj)
1302                 self.tail.value = self.local_tail
1303                 self.wait_event.set()
1304                 self.done.emit(count)
1305
1306 # Fetch more records bar
1307
1308 class FetchMoreRecordsBar():
1309
1310         def __init__(self, model, parent):
1311                 self.model = model
1312
1313                 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1314                 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1315
1316                 self.fetch_count = QSpinBox()
1317                 self.fetch_count.setRange(1, 1000000)
1318                 self.fetch_count.setValue(10)
1319                 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1320
1321                 self.fetch = QPushButton("Go!")
1322                 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1323                 self.fetch.released.connect(self.FetchMoreRecords)
1324
1325                 self.progress = QProgressBar()
1326                 self.progress.setRange(0, 100)
1327                 self.progress.hide()
1328
1329                 self.done_label = QLabel("All records fetched")
1330                 self.done_label.hide()
1331
1332                 self.spacer = QLabel("")
1333
1334                 self.close_button = QToolButton()
1335                 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1336                 self.close_button.released.connect(self.Deactivate)
1337
1338                 self.hbox = QHBoxLayout()
1339                 self.hbox.setContentsMargins(0, 0, 0, 0)
1340
1341                 self.hbox.addWidget(self.label)
1342                 self.hbox.addWidget(self.fetch_count)
1343                 self.hbox.addWidget(self.fetch)
1344                 self.hbox.addWidget(self.spacer)
1345                 self.hbox.addWidget(self.progress)
1346                 self.hbox.addWidget(self.done_label)
1347                 self.hbox.addWidget(self.close_button)
1348
1349                 self.bar = QWidget()
1350                 self.bar.setLayout(self.hbox);
1351                 self.bar.show()
1352
1353                 self.in_progress = False
1354                 self.model.progress.connect(self.Progress)
1355
1356                 self.done = False
1357
1358                 if not model.HasMoreRecords():
1359                         self.Done()
1360
1361         def Widget(self):
1362                 return self.bar
1363
1364         def Activate(self):
1365                 self.bar.show()
1366                 self.fetch.setFocus()
1367
1368         def Deactivate(self):
1369                 self.bar.hide()
1370
1371         def Enable(self, enable):
1372                 self.fetch.setEnabled(enable)
1373                 self.fetch_count.setEnabled(enable)
1374
1375         def Busy(self):
1376                 self.Enable(False)
1377                 self.fetch.hide()
1378                 self.spacer.hide()
1379                 self.progress.show()
1380
1381         def Idle(self):
1382                 self.in_progress = False
1383                 self.Enable(True)
1384                 self.progress.hide()
1385                 self.fetch.show()
1386                 self.spacer.show()
1387
1388         def Target(self):
1389                 return self.fetch_count.value() * glb_chunk_sz
1390
1391         def Done(self):
1392                 self.done = True
1393                 self.Idle()
1394                 self.label.hide()
1395                 self.fetch_count.hide()
1396                 self.fetch.hide()
1397                 self.spacer.hide()
1398                 self.done_label.show()
1399
1400         def Progress(self, count):
1401                 if self.in_progress:
1402                         if count:
1403                                 percent = ((count - self.start) * 100) / self.Target()
1404                                 if percent >= 100:
1405                                         self.Idle()
1406                                 else:
1407                                         self.progress.setValue(percent)
1408                 if not count:
1409                         # Count value of zero means no more records
1410                         self.Done()
1411
1412         def FetchMoreRecords(self):
1413                 if self.done:
1414                         return
1415                 self.progress.setValue(0)
1416                 self.Busy()
1417                 self.in_progress = True
1418                 self.start = self.model.FetchMoreRecords(self.Target())
1419
1420 # Brance data model level two item
1421
1422 class BranchLevelTwoItem():
1423
1424         def __init__(self, row, col, text, parent_item):
1425                 self.row = row
1426                 self.parent_item = parent_item
1427                 self.data = [""] * (col + 1)
1428                 self.data[col] = text
1429                 self.level = 2
1430
1431         def getParentItem(self):
1432                 return self.parent_item
1433
1434         def getRow(self):
1435                 return self.row
1436
1437         def childCount(self):
1438                 return 0
1439
1440         def hasChildren(self):
1441                 return False
1442
1443         def getData(self, column):
1444                 return self.data[column]
1445
1446 # Brance data model level one item
1447
1448 class BranchLevelOneItem():
1449
1450         def __init__(self, glb, row, data, parent_item):
1451                 self.glb = glb
1452                 self.row = row
1453                 self.parent_item = parent_item
1454                 self.child_count = 0
1455                 self.child_items = []
1456                 self.data = data[1:]
1457                 self.dbid = data[0]
1458                 self.level = 1
1459                 self.query_done = False
1460                 self.br_col = len(self.data) - 1
1461
1462         def getChildItem(self, row):
1463                 return self.child_items[row]
1464
1465         def getParentItem(self):
1466                 return self.parent_item
1467
1468         def getRow(self):
1469                 return self.row
1470
1471         def Select(self):
1472                 self.query_done = True
1473
1474                 if not self.glb.have_disassembler:
1475                         return
1476
1477                 query = QSqlQuery(self.glb.db)
1478
1479                 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1480                                   " FROM samples"
1481                                   " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1482                                   " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1483                                   " WHERE samples.id = " + str(self.dbid))
1484                 if not query.next():
1485                         return
1486                 cpu = query.value(0)
1487                 dso = query.value(1)
1488                 sym = query.value(2)
1489                 if dso == 0 or sym == 0:
1490                         return
1491                 off = query.value(3)
1492                 short_name = query.value(4)
1493                 long_name = query.value(5)
1494                 build_id = query.value(6)
1495                 sym_start = query.value(7)
1496                 ip = query.value(8)
1497
1498                 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1499                                   " FROM samples"
1500                                   " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1501                                   " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1502                                   " ORDER BY samples.id"
1503                                   " LIMIT 1")
1504                 if not query.next():
1505                         return
1506                 if query.value(0) != dso:
1507                         # Cannot disassemble from one dso to another
1508                         return
1509                 bsym = query.value(1)
1510                 boff = query.value(2)
1511                 bsym_start = query.value(3)
1512                 if bsym == 0:
1513                         return
1514                 tot = bsym_start + boff + 1 - sym_start - off
1515                 if tot <= 0 or tot > 16384:
1516                         return
1517
1518                 inst = self.glb.disassembler.Instruction()
1519                 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1520                 if not f:
1521                         return
1522                 mode = 0 if Is64Bit(f) else 1
1523                 self.glb.disassembler.SetMode(inst, mode)
1524
1525                 buf_sz = tot + 16
1526                 buf = create_string_buffer(tot + 16)
1527                 f.seek(sym_start + off)
1528                 buf.value = f.read(buf_sz)
1529                 buf_ptr = addressof(buf)
1530                 i = 0
1531                 while tot > 0:
1532                         cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1533                         if cnt:
1534                                 byte_str = tohex(ip).rjust(16)
1535                                 for k in xrange(cnt):
1536                                         byte_str += " %02x" % ord(buf[i])
1537                                         i += 1
1538                                 while k < 15:
1539                                         byte_str += "   "
1540                                         k += 1
1541                                 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
1542                                 self.child_count += 1
1543                         else:
1544                                 return
1545                         buf_ptr += cnt
1546                         tot -= cnt
1547                         buf_sz -= cnt
1548                         ip += cnt
1549
1550         def childCount(self):
1551                 if not self.query_done:
1552                         self.Select()
1553                         if not self.child_count:
1554                                 return -1
1555                 return self.child_count
1556
1557         def hasChildren(self):
1558                 if not self.query_done:
1559                         return True
1560                 return self.child_count > 0
1561
1562         def getData(self, column):
1563                 return self.data[column]
1564
1565 # Brance data model root item
1566
1567 class BranchRootItem():
1568
1569         def __init__(self):
1570                 self.child_count = 0
1571                 self.child_items = []
1572                 self.level = 0
1573
1574         def getChildItem(self, row):
1575                 return self.child_items[row]
1576
1577         def getParentItem(self):
1578                 return None
1579
1580         def getRow(self):
1581                 return 0
1582
1583         def childCount(self):
1584                 return self.child_count
1585
1586         def hasChildren(self):
1587                 return self.child_count > 0
1588
1589         def getData(self, column):
1590                 return ""
1591
1592 # Calculate instructions per cycle
1593
1594 def CalcIPC(cyc_cnt, insn_cnt):
1595         if cyc_cnt and insn_cnt:
1596                 ipc = Decimal(float(insn_cnt) / cyc_cnt)
1597                 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
1598         else:
1599                 ipc = "0"
1600         return ipc
1601
1602 # Branch data preparation
1603
1604 def BranchDataPrepBr(query, data):
1605         data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1606                         " (" + dsoname(query.value(11)) + ")" + " -> " +
1607                         tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1608                         " (" + dsoname(query.value(15)) + ")")
1609
1610 def BranchDataPrepIPC(query, data):
1611         insn_cnt = query.value(16)
1612         cyc_cnt = query.value(17)
1613         ipc = CalcIPC(cyc_cnt, insn_cnt)
1614         data.append(insn_cnt)
1615         data.append(cyc_cnt)
1616         data.append(ipc)
1617
1618 def BranchDataPrep(query):
1619         data = []
1620         for i in xrange(0, 8):
1621                 data.append(query.value(i))
1622         BranchDataPrepBr(query, data)
1623         return data
1624
1625 def BranchDataPrepWA(query):
1626         data = []
1627         data.append(query.value(0))
1628         # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1629         data.append("{:>19}".format(query.value(1)))
1630         for i in xrange(2, 8):
1631                 data.append(query.value(i))
1632         BranchDataPrepBr(query, data)
1633         return data
1634
1635 def BranchDataWithIPCPrep(query):
1636         data = []
1637         for i in xrange(0, 8):
1638                 data.append(query.value(i))
1639         BranchDataPrepIPC(query, data)
1640         BranchDataPrepBr(query, data)
1641         return data
1642
1643 def BranchDataWithIPCPrepWA(query):
1644         data = []
1645         data.append(query.value(0))
1646         # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1647         data.append("{:>19}".format(query.value(1)))
1648         for i in xrange(2, 8):
1649                 data.append(query.value(i))
1650         BranchDataPrepIPC(query, data)
1651         BranchDataPrepBr(query, data)
1652         return data
1653
1654 # Branch data model
1655
1656 class BranchModel(TreeModel):
1657
1658         progress = Signal(object)
1659
1660         def __init__(self, glb, event_id, where_clause, parent=None):
1661                 super(BranchModel, self).__init__(glb, None, parent)
1662                 self.event_id = event_id
1663                 self.more = True
1664                 self.populated = 0
1665                 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
1666                 if self.have_ipc:
1667                         select_ipc = ", insn_count, cyc_count"
1668                         prep_fn = BranchDataWithIPCPrep
1669                         prep_wa_fn = BranchDataWithIPCPrepWA
1670                 else:
1671                         select_ipc = ""
1672                         prep_fn = BranchDataPrep
1673                         prep_wa_fn = BranchDataPrepWA
1674                 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1675                         " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1676                         " ip, symbols.name, sym_offset, dsos.short_name,"
1677                         " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1678                         + select_ipc +
1679                         " FROM samples"
1680                         " INNER JOIN comms ON comm_id = comms.id"
1681                         " INNER JOIN threads ON thread_id = threads.id"
1682                         " INNER JOIN branch_types ON branch_type = branch_types.id"
1683                         " INNER JOIN symbols ON symbol_id = symbols.id"
1684                         " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1685                         " INNER JOIN dsos ON samples.dso_id = dsos.id"
1686                         " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1687                         " WHERE samples.id > $$last_id$$" + where_clause +
1688                         " AND evsel_id = " + str(self.event_id) +
1689                         " ORDER BY samples.id"
1690                         " LIMIT " + str(glb_chunk_sz))
1691                 if pyside_version_1 and sys.version_info[0] == 3:
1692                         prep = prep_fn
1693                 else:
1694                         prep = prep_wa_fn
1695                 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
1696                 self.fetcher.done.connect(self.Update)
1697                 self.fetcher.Fetch(glb_chunk_sz)
1698
1699         def GetRoot(self):
1700                 return BranchRootItem()
1701
1702         def columnCount(self, parent=None):
1703                 if self.have_ipc:
1704                         return 11
1705                 else:
1706                         return 8
1707
1708         def columnHeader(self, column):
1709                 if self.have_ipc:
1710                         return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
1711                 else:
1712                         return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1713
1714         def columnFont(self, column):
1715                 if self.have_ipc:
1716                         br_col = 10
1717                 else:
1718                         br_col = 7
1719                 if column != br_col:
1720                         return None
1721                 return QFont("Monospace")
1722
1723         def DisplayData(self, item, index):
1724                 if item.level == 1:
1725                         self.FetchIfNeeded(item.row)
1726                 return item.getData(index.column())
1727
1728         def AddSample(self, data):
1729                 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1730                 self.root.child_items.append(child)
1731                 self.populated += 1
1732
1733         def Update(self, fetched):
1734                 if not fetched:
1735                         self.more = False
1736                         self.progress.emit(0)
1737                 child_count = self.root.child_count
1738                 count = self.populated - child_count
1739                 if count > 0:
1740                         parent = QModelIndex()
1741                         self.beginInsertRows(parent, child_count, child_count + count - 1)
1742                         self.insertRows(child_count, count, parent)
1743                         self.root.child_count += count
1744                         self.endInsertRows()
1745                         self.progress.emit(self.root.child_count)
1746
1747         def FetchMoreRecords(self, count):
1748                 current = self.root.child_count
1749                 if self.more:
1750                         self.fetcher.Fetch(count)
1751                 else:
1752                         self.progress.emit(0)
1753                 return current
1754
1755         def HasMoreRecords(self):
1756                 return self.more
1757
1758 # Report Variables
1759
1760 class ReportVars():
1761
1762         def __init__(self, name = "", where_clause = "", limit = ""):
1763                 self.name = name
1764                 self.where_clause = where_clause
1765                 self.limit = limit
1766
1767         def UniqueId(self):
1768                 return str(self.where_clause + ";" + self.limit)
1769
1770 # Branch window
1771
1772 class BranchWindow(QMdiSubWindow):
1773
1774         def __init__(self, glb, event_id, report_vars, parent=None):
1775                 super(BranchWindow, self).__init__(parent)
1776
1777                 model_name = "Branch Events " + str(event_id) +  " " + report_vars.UniqueId()
1778
1779                 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1780
1781                 self.view = QTreeView()
1782                 self.view.setUniformRowHeights(True)
1783                 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1784                 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1785                 self.view.setModel(self.model)
1786
1787                 self.ResizeColumnsToContents()
1788
1789                 self.context_menu = TreeContextMenu(self.view)
1790
1791                 self.find_bar = FindBar(self, self, True)
1792
1793                 self.finder = ChildDataItemFinder(self.model.root)
1794
1795                 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1796
1797                 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1798
1799                 self.setWidget(self.vbox.Widget())
1800
1801                 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1802
1803         def ResizeColumnToContents(self, column, n):
1804                 # Using the view's resizeColumnToContents() here is extrememly slow
1805                 # so implement a crude alternative
1806                 mm = "MM" if column else "MMMM"
1807                 font = self.view.font()
1808                 metrics = QFontMetrics(font)
1809                 max = 0
1810                 for row in xrange(n):
1811                         val = self.model.root.child_items[row].data[column]
1812                         len = metrics.width(str(val) + mm)
1813                         max = len if len > max else max
1814                 val = self.model.columnHeader(column)
1815                 len = metrics.width(str(val) + mm)
1816                 max = len if len > max else max
1817                 self.view.setColumnWidth(column, max)
1818
1819         def ResizeColumnsToContents(self):
1820                 n = min(self.model.root.child_count, 100)
1821                 if n < 1:
1822                         # No data yet, so connect a signal to notify when there is
1823                         self.model.rowsInserted.connect(self.UpdateColumnWidths)
1824                         return
1825                 columns = self.model.columnCount()
1826                 for i in xrange(columns):
1827                         self.ResizeColumnToContents(i, n)
1828
1829         def UpdateColumnWidths(self, *x):
1830                 # This only needs to be done once, so disconnect the signal now
1831                 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1832                 self.ResizeColumnsToContents()
1833
1834         def Find(self, value, direction, pattern, context):
1835                 self.view.setFocus()
1836                 self.find_bar.Busy()
1837                 self.finder.Find(value, direction, pattern, context, self.FindDone)
1838
1839         def FindDone(self, row):
1840                 self.find_bar.Idle()
1841                 if row >= 0:
1842                         self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1843                 else:
1844                         self.find_bar.NotFound()
1845
1846 # Line edit data item
1847
1848 class LineEditDataItem(object):
1849
1850         def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1851                 self.glb = glb
1852                 self.label = label
1853                 self.placeholder_text = placeholder_text
1854                 self.parent = parent
1855                 self.id = id
1856
1857                 self.value = default
1858
1859                 self.widget = QLineEdit(default)
1860                 self.widget.editingFinished.connect(self.Validate)
1861                 self.widget.textChanged.connect(self.Invalidate)
1862                 self.red = False
1863                 self.error = ""
1864                 self.validated = True
1865
1866                 if placeholder_text:
1867                         self.widget.setPlaceholderText(placeholder_text)
1868
1869         def TurnTextRed(self):
1870                 if not self.red:
1871                         palette = QPalette()
1872                         palette.setColor(QPalette.Text,Qt.red)
1873                         self.widget.setPalette(palette)
1874                         self.red = True
1875
1876         def TurnTextNormal(self):
1877                 if self.red:
1878                         palette = QPalette()
1879                         self.widget.setPalette(palette)
1880                         self.red = False
1881
1882         def InvalidValue(self, value):
1883                 self.value = ""
1884                 self.TurnTextRed()
1885                 self.error = self.label + " invalid value '" + value + "'"
1886                 self.parent.ShowMessage(self.error)
1887
1888         def Invalidate(self):
1889                 self.validated = False
1890
1891         def DoValidate(self, input_string):
1892                 self.value = input_string.strip()
1893
1894         def Validate(self):
1895                 self.validated = True
1896                 self.error = ""
1897                 self.TurnTextNormal()
1898                 self.parent.ClearMessage()
1899                 input_string = self.widget.text()
1900                 if not len(input_string.strip()):
1901                         self.value = ""
1902                         return
1903                 self.DoValidate(input_string)
1904
1905         def IsValid(self):
1906                 if not self.validated:
1907                         self.Validate()
1908                 if len(self.error):
1909                         self.parent.ShowMessage(self.error)
1910                         return False
1911                 return True
1912
1913         def IsNumber(self, value):
1914                 try:
1915                         x = int(value)
1916                 except:
1917                         x = 0
1918                 return str(x) == value
1919
1920 # Non-negative integer ranges dialog data item
1921
1922 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1923
1924         def __init__(self, glb, label, placeholder_text, column_name, parent):
1925                 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1926
1927                 self.column_name = column_name
1928
1929         def DoValidate(self, input_string):
1930                 singles = []
1931                 ranges = []
1932                 for value in [x.strip() for x in input_string.split(",")]:
1933                         if "-" in value:
1934                                 vrange = value.split("-")
1935                                 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1936                                         return self.InvalidValue(value)
1937                                 ranges.append(vrange)
1938                         else:
1939                                 if not self.IsNumber(value):
1940                                         return self.InvalidValue(value)
1941                                 singles.append(value)
1942                 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1943                 if len(singles):
1944                         ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1945                 self.value = " OR ".join(ranges)
1946
1947 # Positive integer dialog data item
1948
1949 class PositiveIntegerDataItem(LineEditDataItem):
1950
1951         def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1952                 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1953
1954         def DoValidate(self, input_string):
1955                 if not self.IsNumber(input_string.strip()):
1956                         return self.InvalidValue(input_string)
1957                 value = int(input_string.strip())
1958                 if value <= 0:
1959                         return self.InvalidValue(input_string)
1960                 self.value = str(value)
1961
1962 # Dialog data item converted and validated using a SQL table
1963
1964 class SQLTableDataItem(LineEditDataItem):
1965
1966         def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1967                 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1968
1969                 self.table_name = table_name
1970                 self.match_column = match_column
1971                 self.column_name1 = column_name1
1972                 self.column_name2 = column_name2
1973
1974         def ValueToIds(self, value):
1975                 ids = []
1976                 query = QSqlQuery(self.glb.db)
1977                 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1978                 ret = query.exec_(stmt)
1979                 if ret:
1980                         while query.next():
1981                                 ids.append(str(query.value(0)))
1982                 return ids
1983
1984         def DoValidate(self, input_string):
1985                 all_ids = []
1986                 for value in [x.strip() for x in input_string.split(",")]:
1987                         ids = self.ValueToIds(value)
1988                         if len(ids):
1989                                 all_ids.extend(ids)
1990                         else:
1991                                 return self.InvalidValue(value)
1992                 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1993                 if self.column_name2:
1994                         self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1995
1996 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
1997
1998 class SampleTimeRangesDataItem(LineEditDataItem):
1999
2000         def __init__(self, glb, label, placeholder_text, column_name, parent):
2001                 self.column_name = column_name
2002
2003                 self.last_id = 0
2004                 self.first_time = 0
2005                 self.last_time = 2 ** 64
2006
2007                 query = QSqlQuery(glb.db)
2008                 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
2009                 if query.next():
2010                         self.last_id = int(query.value(0))
2011                         self.last_time = int(query.value(1))
2012                 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
2013                 if query.next():
2014                         self.first_time = int(query.value(0))
2015                 if placeholder_text:
2016                         placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
2017
2018                 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
2019
2020         def IdBetween(self, query, lower_id, higher_id, order):
2021                 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
2022                 if query.next():
2023                         return True, int(query.value(0))
2024                 else:
2025                         return False, 0
2026
2027         def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
2028                 query = QSqlQuery(self.glb.db)
2029                 while True:
2030                         next_id = int((lower_id + higher_id) / 2)
2031                         QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
2032                         if not query.next():
2033                                 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
2034                                 if not ok:
2035                                         ok, dbid = self.IdBetween(query, next_id, higher_id, "")
2036                                         if not ok:
2037                                                 return str(higher_id)
2038                                 next_id = dbid
2039                                 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
2040                         next_time = int(query.value(0))
2041                         if get_floor:
2042                                 if target_time > next_time:
2043                                         lower_id = next_id
2044                                 else:
2045                                         higher_id = next_id
2046                                 if higher_id <= lower_id + 1:
2047                                         return str(higher_id)
2048                         else:
2049                                 if target_time >= next_time:
2050                                         lower_id = next_id
2051                                 else:
2052                                         higher_id = next_id
2053                                 if higher_id <= lower_id + 1:
2054                                         return str(lower_id)
2055
2056         def ConvertRelativeTime(self, val):
2057                 mult = 1
2058                 suffix = val[-2:]
2059                 if suffix == "ms":
2060                         mult = 1000000
2061                 elif suffix == "us":
2062                         mult = 1000
2063                 elif suffix == "ns":
2064                         mult = 1
2065                 else:
2066                         return val
2067                 val = val[:-2].strip()
2068                 if not self.IsNumber(val):
2069                         return val
2070                 val = int(val) * mult
2071                 if val >= 0:
2072                         val += self.first_time
2073                 else:
2074                         val += self.last_time
2075                 return str(val)
2076
2077         def ConvertTimeRange(self, vrange):
2078                 if vrange[0] == "":
2079                         vrange[0] = str(self.first_time)
2080                 if vrange[1] == "":
2081                         vrange[1] = str(self.last_time)
2082                 vrange[0] = self.ConvertRelativeTime(vrange[0])
2083                 vrange[1] = self.ConvertRelativeTime(vrange[1])
2084                 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
2085                         return False
2086                 beg_range = max(int(vrange[0]), self.first_time)
2087                 end_range = min(int(vrange[1]), self.last_time)
2088                 if beg_range > self.last_time or end_range < self.first_time:
2089                         return False
2090                 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
2091                 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
2092                 return True
2093
2094         def AddTimeRange(self, value, ranges):
2095                 n = value.count("-")
2096                 if n == 1:
2097                         pass
2098                 elif n == 2:
2099                         if value.split("-")[1].strip() == "":
2100                                 n = 1
2101                 elif n == 3:
2102                         n = 2
2103                 else:
2104                         return False
2105                 pos = findnth(value, "-", n)
2106                 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
2107                 if self.ConvertTimeRange(vrange):
2108                         ranges.append(vrange)
2109                         return True
2110                 return False
2111
2112         def DoValidate(self, input_string):
2113                 ranges = []
2114                 for value in [x.strip() for x in input_string.split(",")]:
2115                         if not self.AddTimeRange(value, ranges):
2116                                 return self.InvalidValue(value)
2117                 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
2118                 self.value = " OR ".join(ranges)
2119
2120 # Report Dialog Base
2121
2122 class ReportDialogBase(QDialog):
2123
2124         def __init__(self, glb, title, items, partial, parent=None):
2125                 super(ReportDialogBase, self).__init__(parent)
2126
2127                 self.glb = glb
2128
2129                 self.report_vars = ReportVars()
2130
2131                 self.setWindowTitle(title)
2132                 self.setMinimumWidth(600)
2133
2134                 self.data_items = [x(glb, self) for x in items]
2135
2136                 self.partial = partial
2137
2138                 self.grid = QGridLayout()
2139
2140                 for row in xrange(len(self.data_items)):
2141                         self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
2142                         self.grid.addWidget(self.data_items[row].widget, row, 1)
2143
2144                 self.status = QLabel()
2145
2146                 self.ok_button = QPushButton("Ok", self)
2147                 self.ok_button.setDefault(True)
2148                 self.ok_button.released.connect(self.Ok)
2149                 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2150
2151                 self.cancel_button = QPushButton("Cancel", self)
2152                 self.cancel_button.released.connect(self.reject)
2153                 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2154
2155                 self.hbox = QHBoxLayout()
2156                 #self.hbox.addStretch()
2157                 self.hbox.addWidget(self.status)
2158                 self.hbox.addWidget(self.ok_button)
2159                 self.hbox.addWidget(self.cancel_button)
2160
2161                 self.vbox = QVBoxLayout()
2162                 self.vbox.addLayout(self.grid)
2163                 self.vbox.addLayout(self.hbox)
2164
2165                 self.setLayout(self.vbox);
2166
2167         def Ok(self):
2168                 vars = self.report_vars
2169                 for d in self.data_items:
2170                         if d.id == "REPORTNAME":
2171                                 vars.name = d.value
2172                 if not vars.name:
2173                         self.ShowMessage("Report name is required")
2174                         return
2175                 for d in self.data_items:
2176                         if not d.IsValid():
2177                                 return
2178                 for d in self.data_items[1:]:
2179                         if d.id == "LIMIT":
2180                                 vars.limit = d.value
2181                         elif len(d.value):
2182                                 if len(vars.where_clause):
2183                                         vars.where_clause += " AND "
2184                                 vars.where_clause += d.value
2185                 if len(vars.where_clause):
2186                         if self.partial:
2187                                 vars.where_clause = " AND ( " + vars.where_clause + " ) "
2188                         else:
2189                                 vars.where_clause = " WHERE " + vars.where_clause + " "
2190                 self.accept()
2191
2192         def ShowMessage(self, msg):
2193                 self.status.setText("<font color=#FF0000>" + msg)
2194
2195         def ClearMessage(self):
2196                 self.status.setText("")
2197
2198 # Selected branch report creation dialog
2199
2200 class SelectedBranchDialog(ReportDialogBase):
2201
2202         def __init__(self, glb, parent=None):
2203                 title = "Selected Branches"
2204                 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2205                          lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2206                          lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2207                          lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2208                          lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2209                          lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2210                          lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2211                          lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2212                          lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2213                 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2214
2215 # Event list
2216
2217 def GetEventList(db):
2218         events = []
2219         query = QSqlQuery(db)
2220         QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2221         while query.next():
2222                 events.append(query.value(0))
2223         return events
2224
2225 # Is a table selectable
2226
2227 def IsSelectable(db, table, sql = "", columns = "*"):
2228         query = QSqlQuery(db)
2229         try:
2230                 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
2231         except:
2232                 return False
2233         return True
2234
2235 # SQL table data model item
2236
2237 class SQLTableItem():
2238
2239         def __init__(self, row, data):
2240                 self.row = row
2241                 self.data = data
2242
2243         def getData(self, column):
2244                 return self.data[column]
2245
2246 # SQL table data model
2247
2248 class SQLTableModel(TableModel):
2249
2250         progress = Signal(object)
2251
2252         def __init__(self, glb, sql, column_headers, parent=None):
2253                 super(SQLTableModel, self).__init__(parent)
2254                 self.glb = glb
2255                 self.more = True
2256                 self.populated = 0
2257                 self.column_headers = column_headers
2258                 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
2259                 self.fetcher.done.connect(self.Update)
2260                 self.fetcher.Fetch(glb_chunk_sz)
2261
2262         def DisplayData(self, item, index):
2263                 self.FetchIfNeeded(item.row)
2264                 return item.getData(index.column())
2265
2266         def AddSample(self, data):
2267                 child = SQLTableItem(self.populated, data)
2268                 self.child_items.append(child)
2269                 self.populated += 1
2270
2271         def Update(self, fetched):
2272                 if not fetched:
2273                         self.more = False
2274                         self.progress.emit(0)
2275                 child_count = self.child_count
2276                 count = self.populated - child_count
2277                 if count > 0:
2278                         parent = QModelIndex()
2279                         self.beginInsertRows(parent, child_count, child_count + count - 1)
2280                         self.insertRows(child_count, count, parent)
2281                         self.child_count += count
2282                         self.endInsertRows()
2283                         self.progress.emit(self.child_count)
2284
2285         def FetchMoreRecords(self, count):
2286                 current = self.child_count
2287                 if self.more:
2288                         self.fetcher.Fetch(count)
2289                 else:
2290                         self.progress.emit(0)
2291                 return current
2292
2293         def HasMoreRecords(self):
2294                 return self.more
2295
2296         def columnCount(self, parent=None):
2297                 return len(self.column_headers)
2298
2299         def columnHeader(self, column):
2300                 return self.column_headers[column]
2301
2302         def SQLTableDataPrep(self, query, count):
2303                 data = []
2304                 for i in xrange(count):
2305                         data.append(query.value(i))
2306                 return data
2307
2308 # SQL automatic table data model
2309
2310 class SQLAutoTableModel(SQLTableModel):
2311
2312         def __init__(self, glb, table_name, parent=None):
2313                 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2314                 if table_name == "comm_threads_view":
2315                         # For now, comm_threads_view has no id column
2316                         sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2317                 column_headers = []
2318                 query = QSqlQuery(glb.db)
2319                 if glb.dbref.is_sqlite3:
2320                         QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2321                         while query.next():
2322                                 column_headers.append(query.value(1))
2323                         if table_name == "sqlite_master":
2324                                 sql = "SELECT * FROM " + table_name
2325                 else:
2326                         if table_name[:19] == "information_schema.":
2327                                 sql = "SELECT * FROM " + table_name
2328                                 select_table_name = table_name[19:]
2329                                 schema = "information_schema"
2330                         else:
2331                                 select_table_name = table_name
2332                                 schema = "public"
2333                         QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2334                         while query.next():
2335                                 column_headers.append(query.value(0))
2336                 if pyside_version_1 and sys.version_info[0] == 3:
2337                         if table_name == "samples_view":
2338                                 self.SQLTableDataPrep = self.samples_view_DataPrep
2339                         if table_name == "samples":
2340                                 self.SQLTableDataPrep = self.samples_DataPrep
2341                 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2342
2343         def samples_view_DataPrep(self, query, count):
2344                 data = []
2345                 data.append(query.value(0))
2346                 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2347                 data.append("{:>19}".format(query.value(1)))
2348                 for i in xrange(2, count):
2349                         data.append(query.value(i))
2350                 return data
2351
2352         def samples_DataPrep(self, query, count):
2353                 data = []
2354                 for i in xrange(9):
2355                         data.append(query.value(i))
2356                 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2357                 data.append("{:>19}".format(query.value(9)))
2358                 for i in xrange(10, count):
2359                         data.append(query.value(i))
2360                 return data
2361
2362 # Base class for custom ResizeColumnsToContents
2363
2364 class ResizeColumnsToContentsBase(QObject):
2365
2366         def __init__(self, parent=None):
2367                 super(ResizeColumnsToContentsBase, self).__init__(parent)
2368
2369         def ResizeColumnToContents(self, column, n):
2370                 # Using the view's resizeColumnToContents() here is extrememly slow
2371                 # so implement a crude alternative
2372                 font = self.view.font()
2373                 metrics = QFontMetrics(font)
2374                 max = 0
2375                 for row in xrange(n):
2376                         val = self.data_model.child_items[row].data[column]
2377                         len = metrics.width(str(val) + "MM")
2378                         max = len if len > max else max
2379                 val = self.data_model.columnHeader(column)
2380                 len = metrics.width(str(val) + "MM")
2381                 max = len if len > max else max
2382                 self.view.setColumnWidth(column, max)
2383
2384         def ResizeColumnsToContents(self):
2385                 n = min(self.data_model.child_count, 100)
2386                 if n < 1:
2387                         # No data yet, so connect a signal to notify when there is
2388                         self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2389                         return
2390                 columns = self.data_model.columnCount()
2391                 for i in xrange(columns):
2392                         self.ResizeColumnToContents(i, n)
2393
2394         def UpdateColumnWidths(self, *x):
2395                 # This only needs to be done once, so disconnect the signal now
2396                 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2397                 self.ResizeColumnsToContents()
2398
2399 # Convert value to CSV
2400
2401 def ToCSValue(val):
2402         if '"' in val:
2403                 val = val.replace('"', '""')
2404         if "," in val or '"' in val:
2405                 val = '"' + val + '"'
2406         return val
2407
2408 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
2409
2410 glb_max_cols = 1000
2411
2412 def RowColumnKey(a):
2413         return a.row() * glb_max_cols + a.column()
2414
2415 # Copy selected table cells to clipboard
2416
2417 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
2418         indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
2419         idx_cnt = len(indexes)
2420         if not idx_cnt:
2421                 return
2422         if idx_cnt == 1:
2423                 with_hdr=False
2424         min_row = indexes[0].row()
2425         max_row = indexes[0].row()
2426         min_col = indexes[0].column()
2427         max_col = indexes[0].column()
2428         for i in indexes:
2429                 min_row = min(min_row, i.row())
2430                 max_row = max(max_row, i.row())
2431                 min_col = min(min_col, i.column())
2432                 max_col = max(max_col, i.column())
2433         if max_col > glb_max_cols:
2434                 raise RuntimeError("glb_max_cols is too low")
2435         max_width = [0] * (1 + max_col - min_col)
2436         for i in indexes:
2437                 c = i.column() - min_col
2438                 max_width[c] = max(max_width[c], len(str(i.data())))
2439         text = ""
2440         pad = ""
2441         sep = ""
2442         if with_hdr:
2443                 model = indexes[0].model()
2444                 for col in range(min_col, max_col + 1):
2445                         val = model.headerData(col, Qt.Horizontal)
2446                         if as_csv:
2447                                 text += sep + ToCSValue(val)
2448                                 sep = ","
2449                         else:
2450                                 c = col - min_col
2451                                 max_width[c] = max(max_width[c], len(val))
2452                                 width = max_width[c]
2453                                 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
2454                                 if align & Qt.AlignRight:
2455                                         val = val.rjust(width)
2456                                 text += pad + sep + val
2457                                 pad = " " * (width - len(val))
2458                                 sep = "  "
2459                 text += "\n"
2460                 pad = ""
2461                 sep = ""
2462         last_row = min_row
2463         for i in indexes:
2464                 if i.row() > last_row:
2465                         last_row = i.row()
2466                         text += "\n"
2467                         pad = ""
2468                         sep = ""
2469                 if as_csv:
2470                         text += sep + ToCSValue(str(i.data()))
2471                         sep = ","
2472                 else:
2473                         width = max_width[i.column() - min_col]
2474                         if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2475                                 val = str(i.data()).rjust(width)
2476                         else:
2477                                 val = str(i.data())
2478                         text += pad + sep + val
2479                         pad = " " * (width - len(val))
2480                         sep = "  "
2481         QApplication.clipboard().setText(text)
2482
2483 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
2484         indexes = view.selectedIndexes()
2485         if not len(indexes):
2486                 return
2487
2488         selection = view.selectionModel()
2489
2490         first = None
2491         for i in indexes:
2492                 above = view.indexAbove(i)
2493                 if not selection.isSelected(above):
2494                         first = i
2495                         break
2496
2497         if first is None:
2498                 raise RuntimeError("CopyTreeCellsToClipboard internal error")
2499
2500         model = first.model()
2501         row_cnt = 0
2502         col_cnt = model.columnCount(first)
2503         max_width = [0] * col_cnt
2504
2505         indent_sz = 2
2506         indent_str = " " * indent_sz
2507
2508         expanded_mark_sz = 2
2509         if sys.version_info[0] == 3:
2510                 expanded_mark = "\u25BC "
2511                 not_expanded_mark = "\u25B6 "
2512         else:
2513                 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
2514                 not_expanded_mark =  unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
2515         leaf_mark = "  "
2516
2517         if not as_csv:
2518                 pos = first
2519                 while True:
2520                         row_cnt += 1
2521                         row = pos.row()
2522                         for c in range(col_cnt):
2523                                 i = pos.sibling(row, c)
2524                                 if c:
2525                                         n = len(str(i.data()))
2526                                 else:
2527                                         n = len(str(i.data()).strip())
2528                                         n += (i.internalPointer().level - 1) * indent_sz
2529                                         n += expanded_mark_sz
2530                                 max_width[c] = max(max_width[c], n)
2531                         pos = view.indexBelow(pos)
2532                         if not selection.isSelected(pos):
2533                                 break
2534
2535         text = ""
2536         pad = ""
2537         sep = ""
2538         if with_hdr:
2539                 for c in range(col_cnt):
2540                         val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
2541                         if as_csv:
2542                                 text += sep + ToCSValue(val)
2543                                 sep = ","
2544                         else:
2545                                 max_width[c] = max(max_width[c], len(val))
2546                                 width = max_width[c]
2547                                 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
2548                                 if align & Qt.AlignRight:
2549                                         val = val.rjust(width)
2550                                 text += pad + sep + val
2551                                 pad = " " * (width - len(val))
2552                                 sep = "   "
2553                 text += "\n"
2554                 pad = ""
2555                 sep = ""
2556
2557         pos = first
2558         while True:
2559                 row = pos.row()
2560                 for c in range(col_cnt):
2561                         i = pos.sibling(row, c)
2562                         val = str(i.data())
2563                         if not c:
2564                                 if model.hasChildren(i):
2565                                         if view.isExpanded(i):
2566                                                 mark = expanded_mark
2567                                         else:
2568                                                 mark = not_expanded_mark
2569                                 else:
2570                                         mark = leaf_mark
2571                                 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
2572                         if as_csv:
2573                                 text += sep + ToCSValue(val)
2574                                 sep = ","
2575                         else:
2576                                 width = max_width[c]
2577                                 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2578                                         val = val.rjust(width)
2579                                 text += pad + sep + val
2580                                 pad = " " * (width - len(val))
2581                                 sep = "   "
2582                 pos = view.indexBelow(pos)
2583                 if not selection.isSelected(pos):
2584                         break
2585                 text = text.rstrip() + "\n"
2586                 pad = ""
2587                 sep = ""
2588
2589         QApplication.clipboard().setText(text)
2590
2591 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
2592         view.CopyCellsToClipboard(view, as_csv, with_hdr)
2593
2594 def CopyCellsToClipboardHdr(view):
2595         CopyCellsToClipboard(view, False, True)
2596
2597 def CopyCellsToClipboardCSV(view):
2598         CopyCellsToClipboard(view, True, True)
2599
2600 # Context menu
2601
2602 class ContextMenu(object):
2603
2604         def __init__(self, view):
2605                 self.view = view
2606                 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
2607                 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
2608
2609         def ShowContextMenu(self, pos):
2610                 menu = QMenu(self.view)
2611                 self.AddActions(menu)
2612                 menu.exec_(self.view.mapToGlobal(pos))
2613
2614         def AddCopy(self, menu):
2615                 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
2616                 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
2617
2618         def AddActions(self, menu):
2619                 self.AddCopy(menu)
2620
2621 class TreeContextMenu(ContextMenu):
2622
2623         def __init__(self, view):
2624                 super(TreeContextMenu, self).__init__(view)
2625
2626         def AddActions(self, menu):
2627                 i = self.view.currentIndex()
2628                 text = str(i.data()).strip()
2629                 if len(text):
2630                         menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
2631                 self.AddCopy(menu)
2632
2633 # Table window
2634
2635 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2636
2637         def __init__(self, glb, table_name, parent=None):
2638                 super(TableWindow, self).__init__(parent)
2639
2640                 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2641
2642                 self.model = QSortFilterProxyModel()
2643                 self.model.setSourceModel(self.data_model)
2644
2645                 self.view = QTableView()
2646                 self.view.setModel(self.model)
2647                 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2648                 self.view.verticalHeader().setVisible(False)
2649                 self.view.sortByColumn(-1, Qt.AscendingOrder)
2650                 self.view.setSortingEnabled(True)
2651                 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2652                 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
2653
2654                 self.ResizeColumnsToContents()
2655
2656                 self.context_menu = ContextMenu(self.view)
2657
2658                 self.find_bar = FindBar(self, self, True)
2659
2660                 self.finder = ChildDataItemFinder(self.data_model)
2661
2662                 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2663
2664                 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2665
2666                 self.setWidget(self.vbox.Widget())
2667
2668                 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2669
2670         def Find(self, value, direction, pattern, context):
2671                 self.view.setFocus()
2672                 self.find_bar.Busy()
2673                 self.finder.Find(value, direction, pattern, context, self.FindDone)
2674
2675         def FindDone(self, row):
2676                 self.find_bar.Idle()
2677                 if row >= 0:
2678                         self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2679                 else:
2680                         self.find_bar.NotFound()
2681
2682 # Table list
2683
2684 def GetTableList(glb):
2685         tables = []
2686         query = QSqlQuery(glb.db)
2687         if glb.dbref.is_sqlite3:
2688                 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2689         else:
2690                 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2691         while query.next():
2692                 tables.append(query.value(0))
2693         if glb.dbref.is_sqlite3:
2694                 tables.append("sqlite_master")
2695         else:
2696                 tables.append("information_schema.tables")
2697                 tables.append("information_schema.views")
2698                 tables.append("information_schema.columns")
2699         return tables
2700
2701 # Top Calls data model
2702
2703 class TopCallsModel(SQLTableModel):
2704
2705         def __init__(self, glb, report_vars, parent=None):
2706                 text = ""
2707                 if not glb.dbref.is_sqlite3:
2708                         text = "::text"
2709                 limit = ""
2710                 if len(report_vars.limit):
2711                         limit = " LIMIT " + report_vars.limit
2712                 sql = ("SELECT comm, pid, tid, name,"
2713                         " CASE"
2714                         " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2715                         " ELSE short_name"
2716                         " END AS dso,"
2717                         " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2718                         " CASE"
2719                         " WHEN (calls.flags = 1) THEN 'no call'" + text +
2720                         " WHEN (calls.flags = 2) THEN 'no return'" + text +
2721                         " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2722                         " ELSE ''" + text +
2723                         " END AS flags"
2724                         " FROM calls"
2725                         " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2726                         " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2727                         " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2728                         " INNER JOIN comms ON calls.comm_id = comms.id"
2729                         " INNER JOIN threads ON calls.thread_id = threads.id" +
2730                         report_vars.where_clause +
2731                         " ORDER BY elapsed_time DESC" +
2732                         limit
2733                         )
2734                 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2735                 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2736                 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2737
2738         def columnAlignment(self, column):
2739                 return self.alignment[column]
2740
2741 # Top Calls report creation dialog
2742
2743 class TopCallsDialog(ReportDialogBase):
2744
2745         def __init__(self, glb, parent=None):
2746                 title = "Top Calls by Elapsed Time"
2747                 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2748                          lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2749                          lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2750                          lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2751                          lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2752                          lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2753                          lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2754                          lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2755                 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2756
2757 # Top Calls window
2758
2759 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2760
2761         def __init__(self, glb, report_vars, parent=None):
2762                 super(TopCallsWindow, self).__init__(parent)
2763
2764                 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2765                 self.model = self.data_model
2766
2767                 self.view = QTableView()
2768                 self.view.setModel(self.model)
2769                 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2770                 self.view.verticalHeader().setVisible(False)
2771                 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2772                 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
2773
2774                 self.context_menu = ContextMenu(self.view)
2775
2776                 self.ResizeColumnsToContents()
2777
2778                 self.find_bar = FindBar(self, self, True)
2779
2780                 self.finder = ChildDataItemFinder(self.model)
2781
2782                 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2783
2784                 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2785
2786                 self.setWidget(self.vbox.Widget())
2787
2788                 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2789
2790         def Find(self, value, direction, pattern, context):
2791                 self.view.setFocus()
2792                 self.find_bar.Busy()
2793                 self.finder.Find(value, direction, pattern, context, self.FindDone)
2794
2795         def FindDone(self, row):
2796                 self.find_bar.Idle()
2797                 if row >= 0:
2798                         self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2799                 else:
2800                         self.find_bar.NotFound()
2801
2802 # Action Definition
2803
2804 def CreateAction(label, tip, callback, parent=None, shortcut=None):
2805         action = QAction(label, parent)
2806         if shortcut != None:
2807                 action.setShortcuts(shortcut)
2808         action.setStatusTip(tip)
2809         action.triggered.connect(callback)
2810         return action
2811
2812 # Typical application actions
2813
2814 def CreateExitAction(app, parent=None):
2815         return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2816
2817 # Typical MDI actions
2818
2819 def CreateCloseActiveWindowAction(mdi_area):
2820         return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2821
2822 def CreateCloseAllWindowsAction(mdi_area):
2823         return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2824
2825 def CreateTileWindowsAction(mdi_area):
2826         return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2827
2828 def CreateCascadeWindowsAction(mdi_area):
2829         return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2830
2831 def CreateNextWindowAction(mdi_area):
2832         return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2833
2834 def CreatePreviousWindowAction(mdi_area):
2835         return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2836
2837 # Typical MDI window menu
2838
2839 class WindowMenu():
2840
2841         def __init__(self, mdi_area, menu):
2842                 self.mdi_area = mdi_area
2843                 self.window_menu = menu.addMenu("&Windows")
2844                 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2845                 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2846                 self.tile_windows = CreateTileWindowsAction(mdi_area)
2847                 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2848                 self.next_window = CreateNextWindowAction(mdi_area)
2849                 self.previous_window = CreatePreviousWindowAction(mdi_area)
2850                 self.window_menu.aboutToShow.connect(self.Update)
2851
2852         def Update(self):
2853                 self.window_menu.clear()
2854                 sub_window_count = len(self.mdi_area.subWindowList())
2855                 have_sub_windows = sub_window_count != 0
2856                 self.close_active_window.setEnabled(have_sub_windows)
2857                 self.close_all_windows.setEnabled(have_sub_windows)
2858                 self.tile_windows.setEnabled(have_sub_windows)
2859                 self.cascade_windows.setEnabled(have_sub_windows)
2860                 self.next_window.setEnabled(have_sub_windows)
2861                 self.previous_window.setEnabled(have_sub_windows)
2862                 self.window_menu.addAction(self.close_active_window)
2863                 self.window_menu.addAction(self.close_all_windows)
2864                 self.window_menu.addSeparator()
2865                 self.window_menu.addAction(self.tile_windows)
2866                 self.window_menu.addAction(self.cascade_windows)
2867                 self.window_menu.addSeparator()
2868                 self.window_menu.addAction(self.next_window)
2869                 self.window_menu.addAction(self.previous_window)
2870                 if sub_window_count == 0:
2871                         return
2872                 self.window_menu.addSeparator()
2873                 nr = 1
2874                 for sub_window in self.mdi_area.subWindowList():
2875                         label = str(nr) + " " + sub_window.name
2876                         if nr < 10:
2877                                 label = "&" + label
2878                         action = self.window_menu.addAction(label)
2879                         action.setCheckable(True)
2880                         action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2881                         action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
2882                         self.window_menu.addAction(action)
2883                         nr += 1
2884
2885         def setActiveSubWindow(self, nr):
2886                 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2887
2888 # Help text
2889
2890 glb_help_text = """
2891 <h1>Contents</h1>
2892 <style>
2893 p.c1 {
2894     text-indent: 40px;
2895 }
2896 p.c2 {
2897     text-indent: 80px;
2898 }
2899 }
2900 </style>
2901 <p class=c1><a href=#reports>1. Reports</a></p>
2902 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2903 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2904 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
2905 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2906 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2907 <p class=c1><a href=#tables>2. Tables</a></p>
2908 <h1 id=reports>1. Reports</h1>
2909 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2910 The result is a GUI window with a tree representing a context-sensitive
2911 call-graph. Expanding a couple of levels of the tree and adjusting column
2912 widths to suit will display something like:
2913 <pre>
2914                                          Call Graph: pt_example
2915 Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
2916 v- ls
2917     v- 2638:2638
2918         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
2919           |- unknown               unknown       1        13198     0.1              1              0.0
2920           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
2921           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
2922           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
2923              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
2924              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
2925              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
2926              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
2927              v- main               ls            1      8182043    99.6         180254             99.9
2928 </pre>
2929 <h3>Points to note:</h3>
2930 <ul>
2931 <li>The top level is a command name (comm)</li>
2932 <li>The next level is a thread (pid:tid)</li>
2933 <li>Subsequent levels are functions</li>
2934 <li>'Count' is the number of calls</li>
2935 <li>'Time' is the elapsed time until the function returns</li>
2936 <li>Percentages are relative to the level above</li>
2937 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
2938 </ul>
2939 <h3>Find</h3>
2940 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2941 The pattern matching symbols are ? for any character and * for zero or more characters.
2942 <h2 id=calltree>1.2 Call Tree</h2>
2943 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2944 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2945 <h2 id=allbranches>1.3 All branches</h2>
2946 The All branches report displays all branches in chronological order.
2947 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2948 <h3>Disassembly</h3>
2949 Open a branch to display disassembly. This only works if:
2950 <ol>
2951 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2952 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2953 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2954 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2955 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2956 </ol>
2957 <h4 id=xed>Intel XED Setup</h4>
2958 To use Intel XED, libxed.so must be present.  To build and install libxed.so:
2959 <pre>
2960 git clone https://github.com/intelxed/mbuild.git mbuild
2961 git clone https://github.com/intelxed/xed
2962 cd xed
2963 ./mfile.py --share
2964 sudo ./mfile.py --prefix=/usr/local install
2965 sudo ldconfig
2966 </pre>
2967 <h3>Instructions per Cycle (IPC)</h3>
2968 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
2969 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
2970 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
2971 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
2972 since the previous displayed 'IPC'.
2973 <h3>Find</h3>
2974 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2975 Refer to Python documentation for the regular expression syntax.
2976 All columns are searched, but only currently fetched rows are searched.
2977 <h2 id=selectedbranches>1.4 Selected branches</h2>
2978 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2979 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2980 <h3>1.4.1 Time ranges</h3>
2981 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2982 ms, us or ns. Also, negative values are relative to the end of trace.  Examples:
2983 <pre>
2984         81073085947329-81073085958238   From 81073085947329 to 81073085958238
2985         100us-200us             From 100us to 200us
2986         10ms-                   From 10ms to the end
2987         -100ns                  The first 100ns
2988         -10ms-                  The last 10ms
2989 </pre>
2990 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
2991 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
2992 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.
2993 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2994 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
2995 <h1 id=tables>2. Tables</h1>
2996 The Tables menu shows all tables and views in the database. Most tables have an associated view
2997 which displays the information in a more friendly way. Not all data for large tables is fetched
2998 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2999 but that can be slow for large tables.
3000 <p>There are also tables of database meta-information.
3001 For SQLite3 databases, the sqlite_master table is included.
3002 For PostgreSQL databases, information_schema.tables/views/columns are included.
3003 <h3>Find</h3>
3004 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
3005 Refer to Python documentation for the regular expression syntax.
3006 All columns are searched, but only currently fetched rows are searched.
3007 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
3008 will go to the next/previous result in id order, instead of display order.
3009 """
3010
3011 # Help window
3012
3013 class HelpWindow(QMdiSubWindow):
3014
3015         def __init__(self, glb, parent=None):
3016                 super(HelpWindow, self).__init__(parent)
3017
3018                 self.text = QTextBrowser()
3019                 self.text.setHtml(glb_help_text)
3020                 self.text.setReadOnly(True)
3021                 self.text.setOpenExternalLinks(True)
3022
3023                 self.setWidget(self.text)
3024
3025                 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
3026
3027 # Main window that only displays the help text
3028
3029 class HelpOnlyWindow(QMainWindow):
3030
3031         def __init__(self, parent=None):
3032                 super(HelpOnlyWindow, self).__init__(parent)
3033
3034                 self.setMinimumSize(200, 100)
3035                 self.resize(800, 600)
3036                 self.setWindowTitle("Exported SQL Viewer Help")
3037                 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
3038
3039                 self.text = QTextBrowser()
3040                 self.text.setHtml(glb_help_text)
3041                 self.text.setReadOnly(True)
3042                 self.text.setOpenExternalLinks(True)
3043
3044                 self.setCentralWidget(self.text)
3045
3046 # PostqreSQL server version
3047
3048 def PostqreSQLServerVersion(db):
3049         query = QSqlQuery(db)
3050         QueryExec(query, "SELECT VERSION()")
3051         if query.next():
3052                 v_str = query.value(0)
3053                 v_list = v_str.strip().split(" ")
3054                 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
3055                         return v_list[1]
3056                 return v_str
3057         return "Unknown"
3058
3059 # SQLite version
3060
3061 def SQLiteVersion(db):
3062         query = QSqlQuery(db)
3063         QueryExec(query, "SELECT sqlite_version()")
3064         if query.next():
3065                 return query.value(0)
3066         return "Unknown"
3067
3068 # About dialog
3069
3070 class AboutDialog(QDialog):
3071
3072         def __init__(self, glb, parent=None):
3073                 super(AboutDialog, self).__init__(parent)
3074
3075                 self.setWindowTitle("About Exported SQL Viewer")
3076                 self.setMinimumWidth(300)
3077
3078                 pyside_version = "1" if pyside_version_1 else "2"
3079
3080                 text = "<pre>"
3081                 text += "Python version:     " + sys.version.split(" ")[0] + "\n"
3082                 text += "PySide version:     " + pyside_version + "\n"
3083                 text += "Qt version:         " + qVersion() + "\n"
3084                 if glb.dbref.is_sqlite3:
3085                         text += "SQLite version:     " + SQLiteVersion(glb.db) + "\n"
3086                 else:
3087                         text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
3088                 text += "</pre>"
3089
3090                 self.text = QTextBrowser()
3091                 self.text.setHtml(text)
3092                 self.text.setReadOnly(True)
3093                 self.text.setOpenExternalLinks(True)
3094
3095                 self.vbox = QVBoxLayout()
3096                 self.vbox.addWidget(self.text)
3097
3098                 self.setLayout(self.vbox);
3099
3100 # Font resize
3101
3102 def ResizeFont(widget, diff):
3103         font = widget.font()
3104         sz = font.pointSize()
3105         font.setPointSize(sz + diff)
3106         widget.setFont(font)
3107
3108 def ShrinkFont(widget):
3109         ResizeFont(widget, -1)
3110
3111 def EnlargeFont(widget):
3112         ResizeFont(widget, 1)
3113
3114 # Unique name for sub-windows
3115
3116 def NumberedWindowName(name, nr):
3117         if nr > 1:
3118                 name += " <" + str(nr) + ">"
3119         return name
3120
3121 def UniqueSubWindowName(mdi_area, name):
3122         nr = 1
3123         while True:
3124                 unique_name = NumberedWindowName(name, nr)
3125                 ok = True
3126                 for sub_window in mdi_area.subWindowList():
3127                         if sub_window.name == unique_name:
3128                                 ok = False
3129                                 break
3130                 if ok:
3131                         return unique_name
3132                 nr += 1
3133
3134 # Add a sub-window
3135
3136 def AddSubWindow(mdi_area, sub_window, name):
3137         unique_name = UniqueSubWindowName(mdi_area, name)
3138         sub_window.setMinimumSize(200, 100)
3139         sub_window.resize(800, 600)
3140         sub_window.setWindowTitle(unique_name)
3141         sub_window.setAttribute(Qt.WA_DeleteOnClose)
3142         sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
3143         sub_window.name = unique_name
3144         mdi_area.addSubWindow(sub_window)
3145         sub_window.show()
3146
3147 # Main window
3148
3149 class MainWindow(QMainWindow):
3150
3151         def __init__(self, glb, parent=None):
3152                 super(MainWindow, self).__init__(parent)
3153
3154                 self.glb = glb
3155
3156                 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
3157                 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
3158                 self.setMinimumSize(200, 100)
3159
3160                 self.mdi_area = QMdiArea()
3161                 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
3162                 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
3163
3164                 self.setCentralWidget(self.mdi_area)
3165
3166                 menu = self.menuBar()
3167
3168                 file_menu = menu.addMenu("&File")
3169                 file_menu.addAction(CreateExitAction(glb.app, self))
3170
3171                 edit_menu = menu.addMenu("&Edit")
3172                 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
3173                 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
3174                 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
3175                 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
3176                 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
3177                 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
3178
3179                 reports_menu = menu.addMenu("&Reports")
3180                 if IsSelectable(glb.db, "calls"):
3181                         reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
3182
3183                 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
3184                         reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
3185
3186                 self.EventMenu(GetEventList(glb.db), reports_menu)
3187
3188                 if IsSelectable(glb.db, "calls"):
3189                         reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
3190
3191                 self.TableMenu(GetTableList(glb), menu)
3192
3193                 self.window_menu = WindowMenu(self.mdi_area, menu)
3194
3195                 help_menu = menu.addMenu("&Help")
3196                 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
3197                 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
3198
3199         def Try(self, fn):
3200                 win = self.mdi_area.activeSubWindow()
3201                 if win:
3202                         try:
3203                                 fn(win.view)
3204                         except:
3205                                 pass
3206
3207         def CopyToClipboard(self):
3208                 self.Try(CopyCellsToClipboardHdr)
3209
3210         def CopyToClipboardCSV(self):
3211                 self.Try(CopyCellsToClipboardCSV)
3212
3213         def Find(self):
3214                 win = self.mdi_area.activeSubWindow()
3215                 if win:
3216                         try:
3217                                 win.find_bar.Activate()
3218                         except:
3219                                 pass
3220
3221         def FetchMoreRecords(self):
3222                 win = self.mdi_area.activeSubWindow()
3223                 if win:
3224                         try:
3225                                 win.fetch_bar.Activate()
3226                         except:
3227                                 pass
3228
3229         def ShrinkFont(self):
3230                 self.Try(ShrinkFont)
3231
3232         def EnlargeFont(self):
3233                 self.Try(EnlargeFont)
3234
3235         def EventMenu(self, events, reports_menu):
3236                 branches_events = 0
3237                 for event in events:
3238                         event = event.split(":")[0]
3239                         if event == "branches":
3240                                 branches_events += 1
3241                 dbid = 0
3242                 for event in events:
3243                         dbid += 1
3244                         event = event.split(":")[0]
3245                         if event == "branches":
3246                                 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
3247                                 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
3248                                 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
3249                                 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
3250
3251         def TableMenu(self, tables, menu):
3252                 table_menu = menu.addMenu("&Tables")
3253                 for table in tables:
3254                         table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
3255
3256         def NewCallGraph(self):
3257                 CallGraphWindow(self.glb, self)
3258
3259         def NewCallTree(self):
3260                 CallTreeWindow(self.glb, self)
3261
3262         def NewTopCalls(self):
3263                 dialog = TopCallsDialog(self.glb, self)
3264                 ret = dialog.exec_()
3265                 if ret:
3266                         TopCallsWindow(self.glb, dialog.report_vars, self)
3267
3268         def NewBranchView(self, event_id):
3269                 BranchWindow(self.glb, event_id, ReportVars(), self)
3270
3271         def NewSelectedBranchView(self, event_id):
3272                 dialog = SelectedBranchDialog(self.glb, self)
3273                 ret = dialog.exec_()
3274                 if ret:
3275                         BranchWindow(self.glb, event_id, dialog.report_vars, self)
3276
3277         def NewTableView(self, table_name):
3278                 TableWindow(self.glb, table_name, self)
3279
3280         def Help(self):
3281                 HelpWindow(self.glb, self)
3282
3283         def About(self):
3284                 dialog = AboutDialog(self.glb, self)
3285                 dialog.exec_()
3286
3287 # XED Disassembler
3288
3289 class xed_state_t(Structure):
3290
3291         _fields_ = [
3292                 ("mode", c_int),
3293                 ("width", c_int)
3294         ]
3295
3296 class XEDInstruction():
3297
3298         def __init__(self, libxed):
3299                 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
3300                 xedd_t = c_byte * 512
3301                 self.xedd = xedd_t()
3302                 self.xedp = addressof(self.xedd)
3303                 libxed.xed_decoded_inst_zero(self.xedp)
3304                 self.state = xed_state_t()
3305                 self.statep = addressof(self.state)
3306                 # Buffer for disassembled instruction text
3307                 self.buffer = create_string_buffer(256)
3308                 self.bufferp = addressof(self.buffer)
3309
3310 class LibXED():
3311
3312         def __init__(self):
3313                 try:
3314                         self.libxed = CDLL("libxed.so")
3315                 except:
3316                         self.libxed = None
3317                 if not self.libxed:
3318                         self.libxed = CDLL("/usr/local/lib/libxed.so")
3319
3320                 self.xed_tables_init = self.libxed.xed_tables_init
3321                 self.xed_tables_init.restype = None
3322                 self.xed_tables_init.argtypes = []
3323
3324                 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
3325                 self.xed_decoded_inst_zero.restype = None
3326                 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
3327
3328                 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
3329                 self.xed_operand_values_set_mode.restype = None
3330                 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
3331
3332                 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
3333                 self.xed_decoded_inst_zero_keep_mode.restype = None
3334                 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
3335
3336                 self.xed_decode = self.libxed.xed_decode
3337                 self.xed_decode.restype = c_int
3338                 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
3339
3340                 self.xed_format_context = self.libxed.xed_format_context
3341                 self.xed_format_context.restype = c_uint
3342                 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
3343
3344                 self.xed_tables_init()
3345
3346         def Instruction(self):
3347                 return XEDInstruction(self)
3348
3349         def SetMode(self, inst, mode):
3350                 if mode:
3351                         inst.state.mode = 4 # 32-bit
3352                         inst.state.width = 4 # 4 bytes
3353                 else:
3354                         inst.state.mode = 1 # 64-bit
3355                         inst.state.width = 8 # 8 bytes
3356                 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
3357
3358         def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
3359                 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
3360                 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
3361                 if err:
3362                         return 0, ""
3363                 # Use AT&T mode (2), alternative is Intel (3)
3364                 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
3365                 if not ok:
3366                         return 0, ""
3367                 if sys.version_info[0] == 2:
3368                         result = inst.buffer.value
3369                 else:
3370                         result = inst.buffer.value.decode()
3371                 # Return instruction length and the disassembled instruction text
3372                 # For now, assume the length is in byte 166
3373                 return inst.xedd[166], result
3374
3375 def TryOpen(file_name):
3376         try:
3377                 return open(file_name, "rb")
3378         except:
3379                 return None
3380
3381 def Is64Bit(f):
3382         result = sizeof(c_void_p)
3383         # ELF support only
3384         pos = f.tell()
3385         f.seek(0)
3386         header = f.read(7)
3387         f.seek(pos)
3388         magic = header[0:4]
3389         if sys.version_info[0] == 2:
3390                 eclass = ord(header[4])
3391                 encoding = ord(header[5])
3392                 version = ord(header[6])
3393         else:
3394                 eclass = header[4]
3395                 encoding = header[5]
3396                 version = header[6]
3397         if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
3398                 result = True if eclass == 2 else False
3399         return result
3400
3401 # Global data
3402
3403 class Glb():
3404
3405         def __init__(self, dbref, db, dbname):
3406                 self.dbref = dbref
3407                 self.db = db
3408                 self.dbname = dbname
3409                 self.home_dir = os.path.expanduser("~")
3410                 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
3411                 if self.buildid_dir:
3412                         self.buildid_dir += "/.build-id/"
3413                 else:
3414                         self.buildid_dir = self.home_dir + "/.debug/.build-id/"
3415                 self.app = None
3416                 self.mainwindow = None
3417                 self.instances_to_shutdown_on_exit = weakref.WeakSet()
3418                 try:
3419                         self.disassembler = LibXED()
3420                         self.have_disassembler = True
3421                 except:
3422                         self.have_disassembler = False
3423
3424         def FileFromBuildId(self, build_id):
3425                 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
3426                 return TryOpen(file_name)
3427
3428         def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
3429                 # Assume current machine i.e. no support for virtualization
3430                 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
3431                         file_name = os.getenv("PERF_KCORE")
3432                         f = TryOpen(file_name) if file_name else None
3433                         if f:
3434                                 return f
3435                         # For now, no special handling if long_name is /proc/kcore
3436                         f = TryOpen(long_name)
3437                         if f:
3438                                 return f
3439                 f = self.FileFromBuildId(build_id)
3440                 if f:
3441                         return f
3442                 return None
3443
3444         def AddInstanceToShutdownOnExit(self, instance):
3445                 self.instances_to_shutdown_on_exit.add(instance)
3446
3447         # Shutdown any background processes or threads
3448         def ShutdownInstances(self):
3449                 for x in self.instances_to_shutdown_on_exit:
3450                         try:
3451                                 x.Shutdown()
3452                         except:
3453                                 pass
3454
3455 # Database reference
3456
3457 class DBRef():
3458
3459         def __init__(self, is_sqlite3, dbname):
3460                 self.is_sqlite3 = is_sqlite3
3461                 self.dbname = dbname
3462
3463         def Open(self, connection_name):
3464                 dbname = self.dbname
3465                 if self.is_sqlite3:
3466                         db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
3467                 else:
3468                         db = QSqlDatabase.addDatabase("QPSQL", connection_name)
3469                         opts = dbname.split()
3470                         for opt in opts:
3471                                 if "=" in opt:
3472                                         opt = opt.split("=")
3473                                         if opt[0] == "hostname":
3474                                                 db.setHostName(opt[1])
3475                                         elif opt[0] == "port":
3476                                                 db.setPort(int(opt[1]))
3477                                         elif opt[0] == "username":
3478                                                 db.setUserName(opt[1])
3479                                         elif opt[0] == "password":
3480                                                 db.setPassword(opt[1])
3481                                         elif opt[0] == "dbname":
3482                                                 dbname = opt[1]
3483                                 else:
3484                                         dbname = opt
3485
3486                 db.setDatabaseName(dbname)
3487                 if not db.open():
3488                         raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
3489                 return db, dbname
3490
3491 # Main
3492
3493 def Main():
3494         usage_str =     "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
3495                         "   or: exported-sql-viewer.py --help-only"
3496         ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
3497         ap.add_argument("--pyside-version-1", action='store_true')
3498         ap.add_argument("dbname", nargs="?")
3499         ap.add_argument("--help-only", action='store_true')
3500         args = ap.parse_args()
3501
3502         if args.help_only:
3503                 app = QApplication(sys.argv)
3504                 mainwindow = HelpOnlyWindow()
3505                 mainwindow.show()
3506                 err = app.exec_()
3507                 sys.exit(err)
3508
3509         dbname = args.dbname
3510         if dbname is None:
3511                 ap.print_usage()
3512                 print("Too few arguments")
3513                 sys.exit(1)
3514
3515         is_sqlite3 = False
3516         try:
3517                 f = open(dbname, "rb")
3518                 if f.read(15) == b'SQLite format 3':
3519                         is_sqlite3 = True
3520                 f.close()
3521         except:
3522                 pass
3523
3524         dbref = DBRef(is_sqlite3, dbname)
3525         db, dbname = dbref.Open("main")
3526         glb = Glb(dbref, db, dbname)
3527         app = QApplication(sys.argv)
3528         glb.app = app
3529         mainwindow = MainWindow(glb)
3530         glb.mainwindow = mainwindow
3531         mainwindow.show()
3532         err = app.exec_()
3533         glb.ShutdownInstances()
3534         db.close()
3535         sys.exit(err)
3536
3537 if __name__ == "__main__":
3538         Main()
This page took 0.260105 seconds and 4 git commands to generate.