OSDN Git Service

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