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