]> git.ipfire.org Git - thirdparty/kernel/stable.git/blob - tools/perf/scripts/python/exported-sql-viewer.py
e38518cdcbc3779c8e001b7c4de969942dd7a5ab
[thirdparty/kernel/stable.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()