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