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