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