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