Loading...
1#!/usr/bin/env python
2# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
5
6# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
8# scripts for details.
9#
10# Following on from the example in the export scripts, a
11# call-graph can be displayed for the pt_example database like this:
12#
13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14#
15# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
17#
18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph. Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24# Call Graph: pt_example
25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
26# v- ls
27# v- 2638:2638
28# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
29# |- unknown unknown 1 13198 0.1 1 0.0
30# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
31# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
32# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
33# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
34# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
35# >- __libc_csu_init ls 1 10354 0.1 10 0.0
36# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
37# v- main ls 1 8182043 99.6 180254 99.9
38#
39# Points to note:
40# The top level is a command name (comm)
41# The next level is a thread (pid:tid)
42# Subsequent levels are functions
43# 'Count' is the number of calls
44# 'Time' is the elapsed time until the function returns
45# Percentages are relative to the level above
46# 'Branch Count' is the total number of branches for that function and all
47# functions that it calls
48
49# There is also a "All branches" report, which displays branches and
50# possibly disassembly. However, presently, the only supported disassembler is
51# Intel XED, and additionally the object code must be present in perf build ID
52# cache. To use Intel XED, libxed.so must be present. To build and install
53# libxed.so:
54# git clone https://github.com/intelxed/mbuild.git mbuild
55# git clone https://github.com/intelxed/xed
56# cd xed
57# ./mfile.py --share
58# sudo ./mfile.py --prefix=/usr/local install
59# sudo ldconfig
60#
61# Example report:
62#
63# Time CPU Command PID TID Branch Type In Tx Branch
64# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65# 7fab593ea260 48 89 e7 mov %rsp, %rdi
66# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68# 7fab593ea260 48 89 e7 mov %rsp, %rdi
69# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
70# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71# 7fab593ea930 55 pushq %rbp
72# 7fab593ea931 48 89 e5 mov %rsp, %rbp
73# 7fab593ea934 41 57 pushq %r15
74# 7fab593ea936 41 56 pushq %r14
75# 7fab593ea938 41 55 pushq %r13
76# 7fab593ea93a 41 54 pushq %r12
77# 7fab593ea93c 53 pushq %rbx
78# 7fab593ea93d 48 89 fb mov %rdi, %rbx
79# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
80# 7fab593ea944 0f 31 rdtsc
81# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
82# 7fab593ea94a 89 c0 mov %eax, %eax
83# 7fab593ea94c 48 09 c2 or %rax, %rdx
84# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
85# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
88# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
89# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
91from __future__ import print_function
92
93import sys
94import argparse
95import weakref
96import threading
97import string
98try:
99 # Python2
100 import cPickle as pickle
101 # size of pickled integer big enough for record size
102 glb_nsz = 8
103except ImportError:
104 import pickle
105 glb_nsz = 16
106import re
107import os
108
109pyside_version_1 = True
110if not "--pyside-version-1" in sys.argv:
111 try:
112 from PySide2.QtCore import *
113 from PySide2.QtGui import *
114 from PySide2.QtSql import *
115 from PySide2.QtWidgets import *
116 pyside_version_1 = False
117 except:
118 pass
119
120if pyside_version_1:
121 from PySide.QtCore import *
122 from PySide.QtGui import *
123 from PySide.QtSql import *
124
125from decimal import *
126from ctypes import *
127from multiprocessing import Process, Array, Value, Event
128
129# xrange is range in Python3
130try:
131 xrange
132except NameError:
133 xrange = range
134
135def printerr(*args, **keyword_args):
136 print(*args, file=sys.stderr, **keyword_args)
137
138# Data formatting helpers
139
140def tohex(ip):
141 if ip < 0:
142 ip += 1 << 64
143 return "%x" % ip
144
145def offstr(offset):
146 if offset:
147 return "+0x%x" % offset
148 return ""
149
150def dsoname(name):
151 if name == "[kernel.kallsyms]":
152 return "[kernel]"
153 return name
154
155def findnth(s, sub, n, offs=0):
156 pos = s.find(sub)
157 if pos < 0:
158 return pos
159 if n <= 1:
160 return offs + pos
161 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
162
163# Percent to one decimal place
164
165def PercentToOneDP(n, d):
166 if not d:
167 return "0.0"
168 x = (n * Decimal(100)) / d
169 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
170
171# Helper for queries that must not fail
172
173def QueryExec(query, stmt):
174 ret = query.exec_(stmt)
175 if not ret:
176 raise Exception("Query failed: " + query.lastError().text())
177
178# Background thread
179
180class Thread(QThread):
181
182 done = Signal(object)
183
184 def __init__(self, task, param=None, parent=None):
185 super(Thread, self).__init__(parent)
186 self.task = task
187 self.param = param
188
189 def run(self):
190 while True:
191 if self.param is None:
192 done, result = self.task()
193 else:
194 done, result = self.task(self.param)
195 self.done.emit(result)
196 if done:
197 break
198
199# Tree data model
200
201class TreeModel(QAbstractItemModel):
202
203 def __init__(self, glb, params, parent=None):
204 super(TreeModel, self).__init__(parent)
205 self.glb = glb
206 self.params = params
207 self.root = self.GetRoot()
208 self.last_row_read = 0
209
210 def Item(self, parent):
211 if parent.isValid():
212 return parent.internalPointer()
213 else:
214 return self.root
215
216 def rowCount(self, parent):
217 result = self.Item(parent).childCount()
218 if result < 0:
219 result = 0
220 self.dataChanged.emit(parent, parent)
221 return result
222
223 def hasChildren(self, parent):
224 return self.Item(parent).hasChildren()
225
226 def headerData(self, section, orientation, role):
227 if role == Qt.TextAlignmentRole:
228 return self.columnAlignment(section)
229 if role != Qt.DisplayRole:
230 return None
231 if orientation != Qt.Horizontal:
232 return None
233 return self.columnHeader(section)
234
235 def parent(self, child):
236 child_item = child.internalPointer()
237 if child_item is self.root:
238 return QModelIndex()
239 parent_item = child_item.getParentItem()
240 return self.createIndex(parent_item.getRow(), 0, parent_item)
241
242 def index(self, row, column, parent):
243 child_item = self.Item(parent).getChildItem(row)
244 return self.createIndex(row, column, child_item)
245
246 def DisplayData(self, item, index):
247 return item.getData(index.column())
248
249 def FetchIfNeeded(self, row):
250 if row > self.last_row_read:
251 self.last_row_read = row
252 if row + 10 >= self.root.child_count:
253 self.fetcher.Fetch(glb_chunk_sz)
254
255 def columnAlignment(self, column):
256 return Qt.AlignLeft
257
258 def columnFont(self, column):
259 return None
260
261 def data(self, index, role):
262 if role == Qt.TextAlignmentRole:
263 return self.columnAlignment(index.column())
264 if role == Qt.FontRole:
265 return self.columnFont(index.column())
266 if role != Qt.DisplayRole:
267 return None
268 item = index.internalPointer()
269 return self.DisplayData(item, index)
270
271# Table data model
272
273class TableModel(QAbstractTableModel):
274
275 def __init__(self, parent=None):
276 super(TableModel, self).__init__(parent)
277 self.child_count = 0
278 self.child_items = []
279 self.last_row_read = 0
280
281 def Item(self, parent):
282 if parent.isValid():
283 return parent.internalPointer()
284 else:
285 return self
286
287 def rowCount(self, parent):
288 return self.child_count
289
290 def headerData(self, section, orientation, role):
291 if role == Qt.TextAlignmentRole:
292 return self.columnAlignment(section)
293 if role != Qt.DisplayRole:
294 return None
295 if orientation != Qt.Horizontal:
296 return None
297 return self.columnHeader(section)
298
299 def index(self, row, column, parent):
300 return self.createIndex(row, column, self.child_items[row])
301
302 def DisplayData(self, item, index):
303 return item.getData(index.column())
304
305 def FetchIfNeeded(self, row):
306 if row > self.last_row_read:
307 self.last_row_read = row
308 if row + 10 >= self.child_count:
309 self.fetcher.Fetch(glb_chunk_sz)
310
311 def columnAlignment(self, column):
312 return Qt.AlignLeft
313
314 def columnFont(self, column):
315 return None
316
317 def data(self, index, role):
318 if role == Qt.TextAlignmentRole:
319 return self.columnAlignment(index.column())
320 if role == Qt.FontRole:
321 return self.columnFont(index.column())
322 if role != Qt.DisplayRole:
323 return None
324 item = index.internalPointer()
325 return self.DisplayData(item, index)
326
327# Model cache
328
329model_cache = weakref.WeakValueDictionary()
330model_cache_lock = threading.Lock()
331
332def LookupCreateModel(model_name, create_fn):
333 model_cache_lock.acquire()
334 try:
335 model = model_cache[model_name]
336 except:
337 model = None
338 if model is None:
339 model = create_fn()
340 model_cache[model_name] = model
341 model_cache_lock.release()
342 return model
343
344# Find bar
345
346class FindBar():
347
348 def __init__(self, parent, finder, is_reg_expr=False):
349 self.finder = finder
350 self.context = []
351 self.last_value = None
352 self.last_pattern = None
353
354 label = QLabel("Find:")
355 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
356
357 self.textbox = QComboBox()
358 self.textbox.setEditable(True)
359 self.textbox.currentIndexChanged.connect(self.ValueChanged)
360
361 self.progress = QProgressBar()
362 self.progress.setRange(0, 0)
363 self.progress.hide()
364
365 if is_reg_expr:
366 self.pattern = QCheckBox("Regular Expression")
367 else:
368 self.pattern = QCheckBox("Pattern")
369 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
370
371 self.next_button = QToolButton()
372 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
373 self.next_button.released.connect(lambda: self.NextPrev(1))
374
375 self.prev_button = QToolButton()
376 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
377 self.prev_button.released.connect(lambda: self.NextPrev(-1))
378
379 self.close_button = QToolButton()
380 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
381 self.close_button.released.connect(self.Deactivate)
382
383 self.hbox = QHBoxLayout()
384 self.hbox.setContentsMargins(0, 0, 0, 0)
385
386 self.hbox.addWidget(label)
387 self.hbox.addWidget(self.textbox)
388 self.hbox.addWidget(self.progress)
389 self.hbox.addWidget(self.pattern)
390 self.hbox.addWidget(self.next_button)
391 self.hbox.addWidget(self.prev_button)
392 self.hbox.addWidget(self.close_button)
393
394 self.bar = QWidget()
395 self.bar.setLayout(self.hbox)
396 self.bar.hide()
397
398 def Widget(self):
399 return self.bar
400
401 def Activate(self):
402 self.bar.show()
403 self.textbox.lineEdit().selectAll()
404 self.textbox.setFocus()
405
406 def Deactivate(self):
407 self.bar.hide()
408
409 def Busy(self):
410 self.textbox.setEnabled(False)
411 self.pattern.hide()
412 self.next_button.hide()
413 self.prev_button.hide()
414 self.progress.show()
415
416 def Idle(self):
417 self.textbox.setEnabled(True)
418 self.progress.hide()
419 self.pattern.show()
420 self.next_button.show()
421 self.prev_button.show()
422
423 def Find(self, direction):
424 value = self.textbox.currentText()
425 pattern = self.pattern.isChecked()
426 self.last_value = value
427 self.last_pattern = pattern
428 self.finder.Find(value, direction, pattern, self.context)
429
430 def ValueChanged(self):
431 value = self.textbox.currentText()
432 pattern = self.pattern.isChecked()
433 index = self.textbox.currentIndex()
434 data = self.textbox.itemData(index)
435 # Store the pattern in the combo box to keep it with the text value
436 if data == None:
437 self.textbox.setItemData(index, pattern)
438 else:
439 self.pattern.setChecked(data)
440 self.Find(0)
441
442 def NextPrev(self, direction):
443 value = self.textbox.currentText()
444 pattern = self.pattern.isChecked()
445 if value != self.last_value:
446 index = self.textbox.findText(value)
447 # Allow for a button press before the value has been added to the combo box
448 if index < 0:
449 index = self.textbox.count()
450 self.textbox.addItem(value, pattern)
451 self.textbox.setCurrentIndex(index)
452 return
453 else:
454 self.textbox.setItemData(index, pattern)
455 elif pattern != self.last_pattern:
456 # Keep the pattern recorded in the combo box up to date
457 index = self.textbox.currentIndex()
458 self.textbox.setItemData(index, pattern)
459 self.Find(direction)
460
461 def NotFound(self):
462 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
463
464# Context-sensitive call graph data model item base
465
466class CallGraphLevelItemBase(object):
467
468 def __init__(self, glb, params, row, parent_item):
469 self.glb = glb
470 self.params = params
471 self.row = row
472 self.parent_item = parent_item
473 self.query_done = False
474 self.child_count = 0
475 self.child_items = []
476 if parent_item:
477 self.level = parent_item.level + 1
478 else:
479 self.level = 0
480
481 def getChildItem(self, row):
482 return self.child_items[row]
483
484 def getParentItem(self):
485 return self.parent_item
486
487 def getRow(self):
488 return self.row
489
490 def childCount(self):
491 if not self.query_done:
492 self.Select()
493 if not self.child_count:
494 return -1
495 return self.child_count
496
497 def hasChildren(self):
498 if not self.query_done:
499 return True
500 return self.child_count > 0
501
502 def getData(self, column):
503 return self.data[column]
504
505# Context-sensitive call graph data model level 2+ item base
506
507class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
508
509 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
510 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
511 self.comm_id = comm_id
512 self.thread_id = thread_id
513 self.call_path_id = call_path_id
514 self.insn_cnt = insn_cnt
515 self.cyc_cnt = cyc_cnt
516 self.branch_count = branch_count
517 self.time = time
518
519 def Select(self):
520 self.query_done = True
521 query = QSqlQuery(self.glb.db)
522 if self.params.have_ipc:
523 ipc_str = ", SUM(insn_count), SUM(cyc_count)"
524 else:
525 ipc_str = ""
526 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
527 " FROM calls"
528 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
529 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
530 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
531 " WHERE parent_call_path_id = " + str(self.call_path_id) +
532 " AND comm_id = " + str(self.comm_id) +
533 " AND thread_id = " + str(self.thread_id) +
534 " GROUP BY call_path_id, name, short_name"
535 " ORDER BY call_path_id")
536 while query.next():
537 if self.params.have_ipc:
538 insn_cnt = int(query.value(5))
539 cyc_cnt = int(query.value(6))
540 branch_count = int(query.value(7))
541 else:
542 insn_cnt = 0
543 cyc_cnt = 0
544 branch_count = int(query.value(5))
545 child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
546 self.child_items.append(child_item)
547 self.child_count += 1
548
549# Context-sensitive call graph data model level three item
550
551class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
552
553 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
554 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
555 dso = dsoname(dso)
556 if self.params.have_ipc:
557 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
558 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
559 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
560 ipc = CalcIPC(cyc_cnt, insn_cnt)
561 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
562 else:
563 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
564 self.dbid = call_path_id
565
566# Context-sensitive call graph data model level two item
567
568class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
569
570 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
571 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
572 if self.params.have_ipc:
573 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
574 else:
575 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
576 self.dbid = thread_id
577
578 def Select(self):
579 super(CallGraphLevelTwoItem, self).Select()
580 for child_item in self.child_items:
581 self.time += child_item.time
582 self.insn_cnt += child_item.insn_cnt
583 self.cyc_cnt += child_item.cyc_cnt
584 self.branch_count += child_item.branch_count
585 for child_item in self.child_items:
586 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
587 if self.params.have_ipc:
588 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
589 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
590 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
591 else:
592 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
593
594# Context-sensitive call graph data model level one item
595
596class CallGraphLevelOneItem(CallGraphLevelItemBase):
597
598 def __init__(self, glb, params, row, comm_id, comm, parent_item):
599 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
600 if self.params.have_ipc:
601 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
602 else:
603 self.data = [comm, "", "", "", "", "", ""]
604 self.dbid = comm_id
605
606 def Select(self):
607 self.query_done = True
608 query = QSqlQuery(self.glb.db)
609 QueryExec(query, "SELECT thread_id, pid, tid"
610 " FROM comm_threads"
611 " INNER JOIN threads ON thread_id = threads.id"
612 " WHERE comm_id = " + str(self.dbid))
613 while query.next():
614 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
615 self.child_items.append(child_item)
616 self.child_count += 1
617
618# Context-sensitive call graph data model root item
619
620class CallGraphRootItem(CallGraphLevelItemBase):
621
622 def __init__(self, glb, params):
623 super(CallGraphRootItem, self).__init__(glb, params, 0, None)
624 self.dbid = 0
625 self.query_done = True
626 if_has_calls = ""
627 if IsSelectable(glb.db, "comms", columns = "has_calls"):
628 if_has_calls = " WHERE has_calls = TRUE"
629 query = QSqlQuery(glb.db)
630 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
631 while query.next():
632 if not query.value(0):
633 continue
634 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
635 self.child_items.append(child_item)
636 self.child_count += 1
637
638# Call graph model parameters
639
640class CallGraphModelParams():
641
642 def __init__(self, glb, parent=None):
643 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
644
645# Context-sensitive call graph data model base
646
647class CallGraphModelBase(TreeModel):
648
649 def __init__(self, glb, parent=None):
650 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
651
652 def FindSelect(self, value, pattern, query):
653 if pattern:
654 # postgresql and sqlite pattern patching differences:
655 # postgresql LIKE is case sensitive but sqlite LIKE is not
656 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
657 # postgresql supports ILIKE which is case insensitive
658 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
659 if not self.glb.dbref.is_sqlite3:
660 # Escape % and _
661 s = value.replace("%", "\%")
662 s = s.replace("_", "\_")
663 # Translate * and ? into SQL LIKE pattern characters % and _
664 trans = string.maketrans("*?", "%_")
665 match = " LIKE '" + str(s).translate(trans) + "'"
666 else:
667 match = " GLOB '" + str(value) + "'"
668 else:
669 match = " = '" + str(value) + "'"
670 self.DoFindSelect(query, match)
671
672 def Found(self, query, found):
673 if found:
674 return self.FindPath(query)
675 return []
676
677 def FindValue(self, value, pattern, query, last_value, last_pattern):
678 if last_value == value and pattern == last_pattern:
679 found = query.first()
680 else:
681 self.FindSelect(value, pattern, query)
682 found = query.next()
683 return self.Found(query, found)
684
685 def FindNext(self, query):
686 found = query.next()
687 if not found:
688 found = query.first()
689 return self.Found(query, found)
690
691 def FindPrev(self, query):
692 found = query.previous()
693 if not found:
694 found = query.last()
695 return self.Found(query, found)
696
697 def FindThread(self, c):
698 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
699 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
700 elif c.direction > 0:
701 ids = self.FindNext(c.query)
702 else:
703 ids = self.FindPrev(c.query)
704 return (True, ids)
705
706 def Find(self, value, direction, pattern, context, callback):
707 class Context():
708 def __init__(self, *x):
709 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
710 def Update(self, *x):
711 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
712 if len(context):
713 context[0].Update(value, direction, pattern)
714 else:
715 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
716 # Use a thread so the UI is not blocked during the SELECT
717 thread = Thread(self.FindThread, context[0])
718 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
719 thread.start()
720
721 def FindDone(self, thread, callback, ids):
722 callback(ids)
723
724# Context-sensitive call graph data model
725
726class CallGraphModel(CallGraphModelBase):
727
728 def __init__(self, glb, parent=None):
729 super(CallGraphModel, self).__init__(glb, parent)
730
731 def GetRoot(self):
732 return CallGraphRootItem(self.glb, self.params)
733
734 def columnCount(self, parent=None):
735 if self.params.have_ipc:
736 return 12
737 else:
738 return 7
739
740 def columnHeader(self, column):
741 if self.params.have_ipc:
742 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
743 else:
744 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
745 return headers[column]
746
747 def columnAlignment(self, column):
748 if self.params.have_ipc:
749 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
750 else:
751 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
752 return alignment[column]
753
754 def DoFindSelect(self, query, match):
755 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
756 " FROM calls"
757 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
758 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
759 " WHERE symbols.name" + match +
760 " GROUP BY comm_id, thread_id, call_path_id"
761 " ORDER BY comm_id, thread_id, call_path_id")
762
763 def FindPath(self, query):
764 # Turn the query result into a list of ids that the tree view can walk
765 # to open the tree at the right place.
766 ids = []
767 parent_id = query.value(0)
768 while parent_id:
769 ids.insert(0, parent_id)
770 q2 = QSqlQuery(self.glb.db)
771 QueryExec(q2, "SELECT parent_id"
772 " FROM call_paths"
773 " WHERE id = " + str(parent_id))
774 if not q2.next():
775 break
776 parent_id = q2.value(0)
777 # The call path root is not used
778 if ids[0] == 1:
779 del ids[0]
780 ids.insert(0, query.value(2))
781 ids.insert(0, query.value(1))
782 return ids
783
784# Call tree data model level 2+ item base
785
786class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
787
788 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
789 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
790 self.comm_id = comm_id
791 self.thread_id = thread_id
792 self.calls_id = calls_id
793 self.insn_cnt = insn_cnt
794 self.cyc_cnt = cyc_cnt
795 self.branch_count = branch_count
796 self.time = time
797
798 def Select(self):
799 self.query_done = True
800 if self.calls_id == 0:
801 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
802 else:
803 comm_thread = ""
804 if self.params.have_ipc:
805 ipc_str = ", insn_count, cyc_count"
806 else:
807 ipc_str = ""
808 query = QSqlQuery(self.glb.db)
809 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
810 " FROM calls"
811 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
812 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
813 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
814 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
815 " ORDER BY call_time, calls.id")
816 while query.next():
817 if self.params.have_ipc:
818 insn_cnt = int(query.value(5))
819 cyc_cnt = int(query.value(6))
820 branch_count = int(query.value(7))
821 else:
822 insn_cnt = 0
823 cyc_cnt = 0
824 branch_count = int(query.value(5))
825 child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
826 self.child_items.append(child_item)
827 self.child_count += 1
828
829# Call tree data model level three item
830
831class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
832
833 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
834 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
835 dso = dsoname(dso)
836 if self.params.have_ipc:
837 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
838 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
839 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
840 ipc = CalcIPC(cyc_cnt, insn_cnt)
841 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
842 else:
843 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
844 self.dbid = calls_id
845
846# Call tree data model level two item
847
848class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
849
850 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
851 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, parent_item)
852 if self.params.have_ipc:
853 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
854 else:
855 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
856 self.dbid = thread_id
857
858 def Select(self):
859 super(CallTreeLevelTwoItem, self).Select()
860 for child_item in self.child_items:
861 self.time += child_item.time
862 self.insn_cnt += child_item.insn_cnt
863 self.cyc_cnt += child_item.cyc_cnt
864 self.branch_count += child_item.branch_count
865 for child_item in self.child_items:
866 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
867 if self.params.have_ipc:
868 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
869 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
870 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
871 else:
872 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
873
874# Call tree data model level one item
875
876class CallTreeLevelOneItem(CallGraphLevelItemBase):
877
878 def __init__(self, glb, params, row, comm_id, comm, parent_item):
879 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
880 if self.params.have_ipc:
881 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
882 else:
883 self.data = [comm, "", "", "", "", "", ""]
884 self.dbid = comm_id
885
886 def Select(self):
887 self.query_done = True
888 query = QSqlQuery(self.glb.db)
889 QueryExec(query, "SELECT thread_id, pid, tid"
890 " FROM comm_threads"
891 " INNER JOIN threads ON thread_id = threads.id"
892 " WHERE comm_id = " + str(self.dbid))
893 while query.next():
894 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
895 self.child_items.append(child_item)
896 self.child_count += 1
897
898# Call tree data model root item
899
900class CallTreeRootItem(CallGraphLevelItemBase):
901
902 def __init__(self, glb, params):
903 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
904 self.dbid = 0
905 self.query_done = True
906 if_has_calls = ""
907 if IsSelectable(glb.db, "comms", columns = "has_calls"):
908 if_has_calls = " WHERE has_calls = TRUE"
909 query = QSqlQuery(glb.db)
910 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
911 while query.next():
912 if not query.value(0):
913 continue
914 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
915 self.child_items.append(child_item)
916 self.child_count += 1
917
918# Call Tree data model
919
920class CallTreeModel(CallGraphModelBase):
921
922 def __init__(self, glb, parent=None):
923 super(CallTreeModel, self).__init__(glb, parent)
924
925 def GetRoot(self):
926 return CallTreeRootItem(self.glb, self.params)
927
928 def columnCount(self, parent=None):
929 if self.params.have_ipc:
930 return 12
931 else:
932 return 7
933
934 def columnHeader(self, column):
935 if self.params.have_ipc:
936 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
937 else:
938 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
939 return headers[column]
940
941 def columnAlignment(self, column):
942 if self.params.have_ipc:
943 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
944 else:
945 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
946 return alignment[column]
947
948 def DoFindSelect(self, query, match):
949 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
950 " FROM calls"
951 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
952 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
953 " WHERE symbols.name" + match +
954 " ORDER BY comm_id, thread_id, call_time, calls.id")
955
956 def FindPath(self, query):
957 # Turn the query result into a list of ids that the tree view can walk
958 # to open the tree at the right place.
959 ids = []
960 parent_id = query.value(0)
961 while parent_id:
962 ids.insert(0, parent_id)
963 q2 = QSqlQuery(self.glb.db)
964 QueryExec(q2, "SELECT parent_id"
965 " FROM calls"
966 " WHERE id = " + str(parent_id))
967 if not q2.next():
968 break
969 parent_id = q2.value(0)
970 ids.insert(0, query.value(2))
971 ids.insert(0, query.value(1))
972 return ids
973
974# Vertical widget layout
975
976class VBox():
977
978 def __init__(self, w1, w2, w3=None):
979 self.vbox = QWidget()
980 self.vbox.setLayout(QVBoxLayout())
981
982 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
983
984 self.vbox.layout().addWidget(w1)
985 self.vbox.layout().addWidget(w2)
986 if w3:
987 self.vbox.layout().addWidget(w3)
988
989 def Widget(self):
990 return self.vbox
991
992# Tree window base
993
994class TreeWindowBase(QMdiSubWindow):
995
996 def __init__(self, parent=None):
997 super(TreeWindowBase, self).__init__(parent)
998
999 self.model = None
1000 self.find_bar = None
1001
1002 self.view = QTreeView()
1003 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1004 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1005
1006 self.context_menu = TreeContextMenu(self.view)
1007
1008 def DisplayFound(self, ids):
1009 if not len(ids):
1010 return False
1011 parent = QModelIndex()
1012 for dbid in ids:
1013 found = False
1014 n = self.model.rowCount(parent)
1015 for row in xrange(n):
1016 child = self.model.index(row, 0, parent)
1017 if child.internalPointer().dbid == dbid:
1018 found = True
1019 self.view.setCurrentIndex(child)
1020 parent = child
1021 break
1022 if not found:
1023 break
1024 return found
1025
1026 def Find(self, value, direction, pattern, context):
1027 self.view.setFocus()
1028 self.find_bar.Busy()
1029 self.model.Find(value, direction, pattern, context, self.FindDone)
1030
1031 def FindDone(self, ids):
1032 found = True
1033 if not self.DisplayFound(ids):
1034 found = False
1035 self.find_bar.Idle()
1036 if not found:
1037 self.find_bar.NotFound()
1038
1039
1040# Context-sensitive call graph window
1041
1042class CallGraphWindow(TreeWindowBase):
1043
1044 def __init__(self, glb, parent=None):
1045 super(CallGraphWindow, self).__init__(parent)
1046
1047 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1048
1049 self.view.setModel(self.model)
1050
1051 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1052 self.view.setColumnWidth(c, w)
1053
1054 self.find_bar = FindBar(self, self)
1055
1056 self.vbox = VBox(self.view, self.find_bar.Widget())
1057
1058 self.setWidget(self.vbox.Widget())
1059
1060 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1061
1062# Call tree window
1063
1064class CallTreeWindow(TreeWindowBase):
1065
1066 def __init__(self, glb, parent=None):
1067 super(CallTreeWindow, self).__init__(parent)
1068
1069 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1070
1071 self.view.setModel(self.model)
1072
1073 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1074 self.view.setColumnWidth(c, w)
1075
1076 self.find_bar = FindBar(self, self)
1077
1078 self.vbox = VBox(self.view, self.find_bar.Widget())
1079
1080 self.setWidget(self.vbox.Widget())
1081
1082 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1083
1084# Child data item finder
1085
1086class ChildDataItemFinder():
1087
1088 def __init__(self, root):
1089 self.root = root
1090 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
1091 self.rows = []
1092 self.pos = 0
1093
1094 def FindSelect(self):
1095 self.rows = []
1096 if self.pattern:
1097 pattern = re.compile(self.value)
1098 for child in self.root.child_items:
1099 for column_data in child.data:
1100 if re.search(pattern, str(column_data)) is not None:
1101 self.rows.append(child.row)
1102 break
1103 else:
1104 for child in self.root.child_items:
1105 for column_data in child.data:
1106 if self.value in str(column_data):
1107 self.rows.append(child.row)
1108 break
1109
1110 def FindValue(self):
1111 self.pos = 0
1112 if self.last_value != self.value or self.pattern != self.last_pattern:
1113 self.FindSelect()
1114 if not len(self.rows):
1115 return -1
1116 return self.rows[self.pos]
1117
1118 def FindThread(self):
1119 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
1120 row = self.FindValue()
1121 elif len(self.rows):
1122 if self.direction > 0:
1123 self.pos += 1
1124 if self.pos >= len(self.rows):
1125 self.pos = 0
1126 else:
1127 self.pos -= 1
1128 if self.pos < 0:
1129 self.pos = len(self.rows) - 1
1130 row = self.rows[self.pos]
1131 else:
1132 row = -1
1133 return (True, row)
1134
1135 def Find(self, value, direction, pattern, context, callback):
1136 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1137 # Use a thread so the UI is not blocked
1138 thread = Thread(self.FindThread)
1139 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1140 thread.start()
1141
1142 def FindDone(self, thread, callback, row):
1143 callback(row)
1144
1145# Number of database records to fetch in one go
1146
1147glb_chunk_sz = 10000
1148
1149# Background process for SQL data fetcher
1150
1151class SQLFetcherProcess():
1152
1153 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1154 # Need a unique connection name
1155 conn_name = "SQLFetcher" + str(os.getpid())
1156 self.db, dbname = dbref.Open(conn_name)
1157 self.sql = sql
1158 self.buffer = buffer
1159 self.head = head
1160 self.tail = tail
1161 self.fetch_count = fetch_count
1162 self.fetching_done = fetching_done
1163 self.process_target = process_target
1164 self.wait_event = wait_event
1165 self.fetched_event = fetched_event
1166 self.prep = prep
1167 self.query = QSqlQuery(self.db)
1168 self.query_limit = 0 if "$$last_id$$" in sql else 2
1169 self.last_id = -1
1170 self.fetched = 0
1171 self.more = True
1172 self.local_head = self.head.value
1173 self.local_tail = self.tail.value
1174
1175 def Select(self):
1176 if self.query_limit:
1177 if self.query_limit == 1:
1178 return
1179 self.query_limit -= 1
1180 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1181 QueryExec(self.query, stmt)
1182
1183 def Next(self):
1184 if not self.query.next():
1185 self.Select()
1186 if not self.query.next():
1187 return None
1188 self.last_id = self.query.value(0)
1189 return self.prep(self.query)
1190
1191 def WaitForTarget(self):
1192 while True:
1193 self.wait_event.clear()
1194 target = self.process_target.value
1195 if target > self.fetched or target < 0:
1196 break
1197 self.wait_event.wait()
1198 return target
1199
1200 def HasSpace(self, sz):
1201 if self.local_tail <= self.local_head:
1202 space = len(self.buffer) - self.local_head
1203 if space > sz:
1204 return True
1205 if space >= glb_nsz:
1206 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1207 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
1208 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1209 self.local_head = 0
1210 if self.local_tail - self.local_head > sz:
1211 return True
1212 return False
1213
1214 def WaitForSpace(self, sz):
1215 if self.HasSpace(sz):
1216 return
1217 while True:
1218 self.wait_event.clear()
1219 self.local_tail = self.tail.value
1220 if self.HasSpace(sz):
1221 return
1222 self.wait_event.wait()
1223
1224 def AddToBuffer(self, obj):
1225 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
1226 n = len(d)
1227 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
1228 sz = n + glb_nsz
1229 self.WaitForSpace(sz)
1230 pos = self.local_head
1231 self.buffer[pos : pos + len(nd)] = nd
1232 self.buffer[pos + glb_nsz : pos + sz] = d
1233 self.local_head += sz
1234
1235 def FetchBatch(self, batch_size):
1236 fetched = 0
1237 while batch_size > fetched:
1238 obj = self.Next()
1239 if obj is None:
1240 self.more = False
1241 break
1242 self.AddToBuffer(obj)
1243 fetched += 1
1244 if fetched:
1245 self.fetched += fetched
1246 with self.fetch_count.get_lock():
1247 self.fetch_count.value += fetched
1248 self.head.value = self.local_head
1249 self.fetched_event.set()
1250
1251 def Run(self):
1252 while self.more:
1253 target = self.WaitForTarget()
1254 if target < 0:
1255 break
1256 batch_size = min(glb_chunk_sz, target - self.fetched)
1257 self.FetchBatch(batch_size)
1258 self.fetching_done.value = True
1259 self.fetched_event.set()
1260
1261def SQLFetcherFn(*x):
1262 process = SQLFetcherProcess(*x)
1263 process.Run()
1264
1265# SQL data fetcher
1266
1267class SQLFetcher(QObject):
1268
1269 done = Signal(object)
1270
1271 def __init__(self, glb, sql, prep, process_data, parent=None):
1272 super(SQLFetcher, self).__init__(parent)
1273 self.process_data = process_data
1274 self.more = True
1275 self.target = 0
1276 self.last_target = 0
1277 self.fetched = 0
1278 self.buffer_size = 16 * 1024 * 1024
1279 self.buffer = Array(c_char, self.buffer_size, lock=False)
1280 self.head = Value(c_longlong)
1281 self.tail = Value(c_longlong)
1282 self.local_tail = 0
1283 self.fetch_count = Value(c_longlong)
1284 self.fetching_done = Value(c_bool)
1285 self.last_count = 0
1286 self.process_target = Value(c_longlong)
1287 self.wait_event = Event()
1288 self.fetched_event = Event()
1289 glb.AddInstanceToShutdownOnExit(self)
1290 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))
1291 self.process.start()
1292 self.thread = Thread(self.Thread)
1293 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1294 self.thread.start()
1295
1296 def Shutdown(self):
1297 # Tell the thread and process to exit
1298 self.process_target.value = -1
1299 self.wait_event.set()
1300 self.more = False
1301 self.fetching_done.value = True
1302 self.fetched_event.set()
1303
1304 def Thread(self):
1305 if not self.more:
1306 return True, 0
1307 while True:
1308 self.fetched_event.clear()
1309 fetch_count = self.fetch_count.value
1310 if fetch_count != self.last_count:
1311 break
1312 if self.fetching_done.value:
1313 self.more = False
1314 return True, 0
1315 self.fetched_event.wait()
1316 count = fetch_count - self.last_count
1317 self.last_count = fetch_count
1318 self.fetched += count
1319 return False, count
1320
1321 def Fetch(self, nr):
1322 if not self.more:
1323 # -1 inidcates there are no more
1324 return -1
1325 result = self.fetched
1326 extra = result + nr - self.target
1327 if extra > 0:
1328 self.target += extra
1329 # process_target < 0 indicates shutting down
1330 if self.process_target.value >= 0:
1331 self.process_target.value = self.target
1332 self.wait_event.set()
1333 return result
1334
1335 def RemoveFromBuffer(self):
1336 pos = self.local_tail
1337 if len(self.buffer) - pos < glb_nsz:
1338 pos = 0
1339 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
1340 if n == 0:
1341 pos = 0
1342 n = pickle.loads(self.buffer[0 : glb_nsz])
1343 pos += glb_nsz
1344 obj = pickle.loads(self.buffer[pos : pos + n])
1345 self.local_tail = pos + n
1346 return obj
1347
1348 def ProcessData(self, count):
1349 for i in xrange(count):
1350 obj = self.RemoveFromBuffer()
1351 self.process_data(obj)
1352 self.tail.value = self.local_tail
1353 self.wait_event.set()
1354 self.done.emit(count)
1355
1356# Fetch more records bar
1357
1358class FetchMoreRecordsBar():
1359
1360 def __init__(self, model, parent):
1361 self.model = model
1362
1363 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1364 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1365
1366 self.fetch_count = QSpinBox()
1367 self.fetch_count.setRange(1, 1000000)
1368 self.fetch_count.setValue(10)
1369 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1370
1371 self.fetch = QPushButton("Go!")
1372 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1373 self.fetch.released.connect(self.FetchMoreRecords)
1374
1375 self.progress = QProgressBar()
1376 self.progress.setRange(0, 100)
1377 self.progress.hide()
1378
1379 self.done_label = QLabel("All records fetched")
1380 self.done_label.hide()
1381
1382 self.spacer = QLabel("")
1383
1384 self.close_button = QToolButton()
1385 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1386 self.close_button.released.connect(self.Deactivate)
1387
1388 self.hbox = QHBoxLayout()
1389 self.hbox.setContentsMargins(0, 0, 0, 0)
1390
1391 self.hbox.addWidget(self.label)
1392 self.hbox.addWidget(self.fetch_count)
1393 self.hbox.addWidget(self.fetch)
1394 self.hbox.addWidget(self.spacer)
1395 self.hbox.addWidget(self.progress)
1396 self.hbox.addWidget(self.done_label)
1397 self.hbox.addWidget(self.close_button)
1398
1399 self.bar = QWidget()
1400 self.bar.setLayout(self.hbox)
1401 self.bar.show()
1402
1403 self.in_progress = False
1404 self.model.progress.connect(self.Progress)
1405
1406 self.done = False
1407
1408 if not model.HasMoreRecords():
1409 self.Done()
1410
1411 def Widget(self):
1412 return self.bar
1413
1414 def Activate(self):
1415 self.bar.show()
1416 self.fetch.setFocus()
1417
1418 def Deactivate(self):
1419 self.bar.hide()
1420
1421 def Enable(self, enable):
1422 self.fetch.setEnabled(enable)
1423 self.fetch_count.setEnabled(enable)
1424
1425 def Busy(self):
1426 self.Enable(False)
1427 self.fetch.hide()
1428 self.spacer.hide()
1429 self.progress.show()
1430
1431 def Idle(self):
1432 self.in_progress = False
1433 self.Enable(True)
1434 self.progress.hide()
1435 self.fetch.show()
1436 self.spacer.show()
1437
1438 def Target(self):
1439 return self.fetch_count.value() * glb_chunk_sz
1440
1441 def Done(self):
1442 self.done = True
1443 self.Idle()
1444 self.label.hide()
1445 self.fetch_count.hide()
1446 self.fetch.hide()
1447 self.spacer.hide()
1448 self.done_label.show()
1449
1450 def Progress(self, count):
1451 if self.in_progress:
1452 if count:
1453 percent = ((count - self.start) * 100) / self.Target()
1454 if percent >= 100:
1455 self.Idle()
1456 else:
1457 self.progress.setValue(percent)
1458 if not count:
1459 # Count value of zero means no more records
1460 self.Done()
1461
1462 def FetchMoreRecords(self):
1463 if self.done:
1464 return
1465 self.progress.setValue(0)
1466 self.Busy()
1467 self.in_progress = True
1468 self.start = self.model.FetchMoreRecords(self.Target())
1469
1470# Brance data model level two item
1471
1472class BranchLevelTwoItem():
1473
1474 def __init__(self, row, col, text, parent_item):
1475 self.row = row
1476 self.parent_item = parent_item
1477 self.data = [""] * (col + 1)
1478 self.data[col] = text
1479 self.level = 2
1480
1481 def getParentItem(self):
1482 return self.parent_item
1483
1484 def getRow(self):
1485 return self.row
1486
1487 def childCount(self):
1488 return 0
1489
1490 def hasChildren(self):
1491 return False
1492
1493 def getData(self, column):
1494 return self.data[column]
1495
1496# Brance data model level one item
1497
1498class BranchLevelOneItem():
1499
1500 def __init__(self, glb, row, data, parent_item):
1501 self.glb = glb
1502 self.row = row
1503 self.parent_item = parent_item
1504 self.child_count = 0
1505 self.child_items = []
1506 self.data = data[1:]
1507 self.dbid = data[0]
1508 self.level = 1
1509 self.query_done = False
1510 self.br_col = len(self.data) - 1
1511
1512 def getChildItem(self, row):
1513 return self.child_items[row]
1514
1515 def getParentItem(self):
1516 return self.parent_item
1517
1518 def getRow(self):
1519 return self.row
1520
1521 def Select(self):
1522 self.query_done = True
1523
1524 if not self.glb.have_disassembler:
1525 return
1526
1527 query = QSqlQuery(self.glb.db)
1528
1529 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1530 " FROM samples"
1531 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1532 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1533 " WHERE samples.id = " + str(self.dbid))
1534 if not query.next():
1535 return
1536 cpu = query.value(0)
1537 dso = query.value(1)
1538 sym = query.value(2)
1539 if dso == 0 or sym == 0:
1540 return
1541 off = query.value(3)
1542 short_name = query.value(4)
1543 long_name = query.value(5)
1544 build_id = query.value(6)
1545 sym_start = query.value(7)
1546 ip = query.value(8)
1547
1548 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1549 " FROM samples"
1550 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1551 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1552 " ORDER BY samples.id"
1553 " LIMIT 1")
1554 if not query.next():
1555 return
1556 if query.value(0) != dso:
1557 # Cannot disassemble from one dso to another
1558 return
1559 bsym = query.value(1)
1560 boff = query.value(2)
1561 bsym_start = query.value(3)
1562 if bsym == 0:
1563 return
1564 tot = bsym_start + boff + 1 - sym_start - off
1565 if tot <= 0 or tot > 16384:
1566 return
1567
1568 inst = self.glb.disassembler.Instruction()
1569 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1570 if not f:
1571 return
1572 mode = 0 if Is64Bit(f) else 1
1573 self.glb.disassembler.SetMode(inst, mode)
1574
1575 buf_sz = tot + 16
1576 buf = create_string_buffer(tot + 16)
1577 f.seek(sym_start + off)
1578 buf.value = f.read(buf_sz)
1579 buf_ptr = addressof(buf)
1580 i = 0
1581 while tot > 0:
1582 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1583 if cnt:
1584 byte_str = tohex(ip).rjust(16)
1585 for k in xrange(cnt):
1586 byte_str += " %02x" % ord(buf[i])
1587 i += 1
1588 while k < 15:
1589 byte_str += " "
1590 k += 1
1591 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
1592 self.child_count += 1
1593 else:
1594 return
1595 buf_ptr += cnt
1596 tot -= cnt
1597 buf_sz -= cnt
1598 ip += cnt
1599
1600 def childCount(self):
1601 if not self.query_done:
1602 self.Select()
1603 if not self.child_count:
1604 return -1
1605 return self.child_count
1606
1607 def hasChildren(self):
1608 if not self.query_done:
1609 return True
1610 return self.child_count > 0
1611
1612 def getData(self, column):
1613 return self.data[column]
1614
1615# Brance data model root item
1616
1617class BranchRootItem():
1618
1619 def __init__(self):
1620 self.child_count = 0
1621 self.child_items = []
1622 self.level = 0
1623
1624 def getChildItem(self, row):
1625 return self.child_items[row]
1626
1627 def getParentItem(self):
1628 return None
1629
1630 def getRow(self):
1631 return 0
1632
1633 def childCount(self):
1634 return self.child_count
1635
1636 def hasChildren(self):
1637 return self.child_count > 0
1638
1639 def getData(self, column):
1640 return ""
1641
1642# Calculate instructions per cycle
1643
1644def CalcIPC(cyc_cnt, insn_cnt):
1645 if cyc_cnt and insn_cnt:
1646 ipc = Decimal(float(insn_cnt) / cyc_cnt)
1647 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
1648 else:
1649 ipc = "0"
1650 return ipc
1651
1652# Branch data preparation
1653
1654def BranchDataPrepBr(query, data):
1655 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1656 " (" + dsoname(query.value(11)) + ")" + " -> " +
1657 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1658 " (" + dsoname(query.value(15)) + ")")
1659
1660def BranchDataPrepIPC(query, data):
1661 insn_cnt = query.value(16)
1662 cyc_cnt = query.value(17)
1663 ipc = CalcIPC(cyc_cnt, insn_cnt)
1664 data.append(insn_cnt)
1665 data.append(cyc_cnt)
1666 data.append(ipc)
1667
1668def BranchDataPrep(query):
1669 data = []
1670 for i in xrange(0, 8):
1671 data.append(query.value(i))
1672 BranchDataPrepBr(query, data)
1673 return data
1674
1675def BranchDataPrepWA(query):
1676 data = []
1677 data.append(query.value(0))
1678 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1679 data.append("{:>19}".format(query.value(1)))
1680 for i in xrange(2, 8):
1681 data.append(query.value(i))
1682 BranchDataPrepBr(query, data)
1683 return data
1684
1685def BranchDataWithIPCPrep(query):
1686 data = []
1687 for i in xrange(0, 8):
1688 data.append(query.value(i))
1689 BranchDataPrepIPC(query, data)
1690 BranchDataPrepBr(query, data)
1691 return data
1692
1693def BranchDataWithIPCPrepWA(query):
1694 data = []
1695 data.append(query.value(0))
1696 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1697 data.append("{:>19}".format(query.value(1)))
1698 for i in xrange(2, 8):
1699 data.append(query.value(i))
1700 BranchDataPrepIPC(query, data)
1701 BranchDataPrepBr(query, data)
1702 return data
1703
1704# Branch data model
1705
1706class BranchModel(TreeModel):
1707
1708 progress = Signal(object)
1709
1710 def __init__(self, glb, event_id, where_clause, parent=None):
1711 super(BranchModel, self).__init__(glb, None, parent)
1712 self.event_id = event_id
1713 self.more = True
1714 self.populated = 0
1715 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
1716 if self.have_ipc:
1717 select_ipc = ", insn_count, cyc_count"
1718 prep_fn = BranchDataWithIPCPrep
1719 prep_wa_fn = BranchDataWithIPCPrepWA
1720 else:
1721 select_ipc = ""
1722 prep_fn = BranchDataPrep
1723 prep_wa_fn = BranchDataPrepWA
1724 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1725 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1726 " ip, symbols.name, sym_offset, dsos.short_name,"
1727 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1728 + select_ipc +
1729 " FROM samples"
1730 " INNER JOIN comms ON comm_id = comms.id"
1731 " INNER JOIN threads ON thread_id = threads.id"
1732 " INNER JOIN branch_types ON branch_type = branch_types.id"
1733 " INNER JOIN symbols ON symbol_id = symbols.id"
1734 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1735 " INNER JOIN dsos ON samples.dso_id = dsos.id"
1736 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1737 " WHERE samples.id > $$last_id$$" + where_clause +
1738 " AND evsel_id = " + str(self.event_id) +
1739 " ORDER BY samples.id"
1740 " LIMIT " + str(glb_chunk_sz))
1741 if pyside_version_1 and sys.version_info[0] == 3:
1742 prep = prep_fn
1743 else:
1744 prep = prep_wa_fn
1745 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
1746 self.fetcher.done.connect(self.Update)
1747 self.fetcher.Fetch(glb_chunk_sz)
1748
1749 def GetRoot(self):
1750 return BranchRootItem()
1751
1752 def columnCount(self, parent=None):
1753 if self.have_ipc:
1754 return 11
1755 else:
1756 return 8
1757
1758 def columnHeader(self, column):
1759 if self.have_ipc:
1760 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
1761 else:
1762 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1763
1764 def columnFont(self, column):
1765 if self.have_ipc:
1766 br_col = 10
1767 else:
1768 br_col = 7
1769 if column != br_col:
1770 return None
1771 return QFont("Monospace")
1772
1773 def DisplayData(self, item, index):
1774 if item.level == 1:
1775 self.FetchIfNeeded(item.row)
1776 return item.getData(index.column())
1777
1778 def AddSample(self, data):
1779 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1780 self.root.child_items.append(child)
1781 self.populated += 1
1782
1783 def Update(self, fetched):
1784 if not fetched:
1785 self.more = False
1786 self.progress.emit(0)
1787 child_count = self.root.child_count
1788 count = self.populated - child_count
1789 if count > 0:
1790 parent = QModelIndex()
1791 self.beginInsertRows(parent, child_count, child_count + count - 1)
1792 self.insertRows(child_count, count, parent)
1793 self.root.child_count += count
1794 self.endInsertRows()
1795 self.progress.emit(self.root.child_count)
1796
1797 def FetchMoreRecords(self, count):
1798 current = self.root.child_count
1799 if self.more:
1800 self.fetcher.Fetch(count)
1801 else:
1802 self.progress.emit(0)
1803 return current
1804
1805 def HasMoreRecords(self):
1806 return self.more
1807
1808# Report Variables
1809
1810class ReportVars():
1811
1812 def __init__(self, name = "", where_clause = "", limit = ""):
1813 self.name = name
1814 self.where_clause = where_clause
1815 self.limit = limit
1816
1817 def UniqueId(self):
1818 return str(self.where_clause + ";" + self.limit)
1819
1820# Branch window
1821
1822class BranchWindow(QMdiSubWindow):
1823
1824 def __init__(self, glb, event_id, report_vars, parent=None):
1825 super(BranchWindow, self).__init__(parent)
1826
1827 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
1828
1829 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1830
1831 self.view = QTreeView()
1832 self.view.setUniformRowHeights(True)
1833 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1834 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1835 self.view.setModel(self.model)
1836
1837 self.ResizeColumnsToContents()
1838
1839 self.context_menu = TreeContextMenu(self.view)
1840
1841 self.find_bar = FindBar(self, self, True)
1842
1843 self.finder = ChildDataItemFinder(self.model.root)
1844
1845 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1846
1847 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1848
1849 self.setWidget(self.vbox.Widget())
1850
1851 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1852
1853 def ResizeColumnToContents(self, column, n):
1854 # Using the view's resizeColumnToContents() here is extrememly slow
1855 # so implement a crude alternative
1856 mm = "MM" if column else "MMMM"
1857 font = self.view.font()
1858 metrics = QFontMetrics(font)
1859 max = 0
1860 for row in xrange(n):
1861 val = self.model.root.child_items[row].data[column]
1862 len = metrics.width(str(val) + mm)
1863 max = len if len > max else max
1864 val = self.model.columnHeader(column)
1865 len = metrics.width(str(val) + mm)
1866 max = len if len > max else max
1867 self.view.setColumnWidth(column, max)
1868
1869 def ResizeColumnsToContents(self):
1870 n = min(self.model.root.child_count, 100)
1871 if n < 1:
1872 # No data yet, so connect a signal to notify when there is
1873 self.model.rowsInserted.connect(self.UpdateColumnWidths)
1874 return
1875 columns = self.model.columnCount()
1876 for i in xrange(columns):
1877 self.ResizeColumnToContents(i, n)
1878
1879 def UpdateColumnWidths(self, *x):
1880 # This only needs to be done once, so disconnect the signal now
1881 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1882 self.ResizeColumnsToContents()
1883
1884 def Find(self, value, direction, pattern, context):
1885 self.view.setFocus()
1886 self.find_bar.Busy()
1887 self.finder.Find(value, direction, pattern, context, self.FindDone)
1888
1889 def FindDone(self, row):
1890 self.find_bar.Idle()
1891 if row >= 0:
1892 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1893 else:
1894 self.find_bar.NotFound()
1895
1896# Line edit data item
1897
1898class LineEditDataItem(object):
1899
1900 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1901 self.glb = glb
1902 self.label = label
1903 self.placeholder_text = placeholder_text
1904 self.parent = parent
1905 self.id = id
1906
1907 self.value = default
1908
1909 self.widget = QLineEdit(default)
1910 self.widget.editingFinished.connect(self.Validate)
1911 self.widget.textChanged.connect(self.Invalidate)
1912 self.red = False
1913 self.error = ""
1914 self.validated = True
1915
1916 if placeholder_text:
1917 self.widget.setPlaceholderText(placeholder_text)
1918
1919 def TurnTextRed(self):
1920 if not self.red:
1921 palette = QPalette()
1922 palette.setColor(QPalette.Text,Qt.red)
1923 self.widget.setPalette(palette)
1924 self.red = True
1925
1926 def TurnTextNormal(self):
1927 if self.red:
1928 palette = QPalette()
1929 self.widget.setPalette(palette)
1930 self.red = False
1931
1932 def InvalidValue(self, value):
1933 self.value = ""
1934 self.TurnTextRed()
1935 self.error = self.label + " invalid value '" + value + "'"
1936 self.parent.ShowMessage(self.error)
1937
1938 def Invalidate(self):
1939 self.validated = False
1940
1941 def DoValidate(self, input_string):
1942 self.value = input_string.strip()
1943
1944 def Validate(self):
1945 self.validated = True
1946 self.error = ""
1947 self.TurnTextNormal()
1948 self.parent.ClearMessage()
1949 input_string = self.widget.text()
1950 if not len(input_string.strip()):
1951 self.value = ""
1952 return
1953 self.DoValidate(input_string)
1954
1955 def IsValid(self):
1956 if not self.validated:
1957 self.Validate()
1958 if len(self.error):
1959 self.parent.ShowMessage(self.error)
1960 return False
1961 return True
1962
1963 def IsNumber(self, value):
1964 try:
1965 x = int(value)
1966 except:
1967 x = 0
1968 return str(x) == value
1969
1970# Non-negative integer ranges dialog data item
1971
1972class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1973
1974 def __init__(self, glb, label, placeholder_text, column_name, parent):
1975 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1976
1977 self.column_name = column_name
1978
1979 def DoValidate(self, input_string):
1980 singles = []
1981 ranges = []
1982 for value in [x.strip() for x in input_string.split(",")]:
1983 if "-" in value:
1984 vrange = value.split("-")
1985 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1986 return self.InvalidValue(value)
1987 ranges.append(vrange)
1988 else:
1989 if not self.IsNumber(value):
1990 return self.InvalidValue(value)
1991 singles.append(value)
1992 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1993 if len(singles):
1994 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1995 self.value = " OR ".join(ranges)
1996
1997# Positive integer dialog data item
1998
1999class PositiveIntegerDataItem(LineEditDataItem):
2000
2001 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
2002 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
2003
2004 def DoValidate(self, input_string):
2005 if not self.IsNumber(input_string.strip()):
2006 return self.InvalidValue(input_string)
2007 value = int(input_string.strip())
2008 if value <= 0:
2009 return self.InvalidValue(input_string)
2010 self.value = str(value)
2011
2012# Dialog data item converted and validated using a SQL table
2013
2014class SQLTableDataItem(LineEditDataItem):
2015
2016 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
2017 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
2018
2019 self.table_name = table_name
2020 self.match_column = match_column
2021 self.column_name1 = column_name1
2022 self.column_name2 = column_name2
2023
2024 def ValueToIds(self, value):
2025 ids = []
2026 query = QSqlQuery(self.glb.db)
2027 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
2028 ret = query.exec_(stmt)
2029 if ret:
2030 while query.next():
2031 ids.append(str(query.value(0)))
2032 return ids
2033
2034 def DoValidate(self, input_string):
2035 all_ids = []
2036 for value in [x.strip() for x in input_string.split(",")]:
2037 ids = self.ValueToIds(value)
2038 if len(ids):
2039 all_ids.extend(ids)
2040 else:
2041 return self.InvalidValue(value)
2042 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
2043 if self.column_name2:
2044 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
2045
2046# Sample time ranges dialog data item converted and validated using 'samples' SQL table
2047
2048class SampleTimeRangesDataItem(LineEditDataItem):
2049
2050 def __init__(self, glb, label, placeholder_text, column_name, parent):
2051 self.column_name = column_name
2052
2053 self.last_id = 0
2054 self.first_time = 0
2055 self.last_time = 2 ** 64
2056
2057 query = QSqlQuery(glb.db)
2058 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
2059 if query.next():
2060 self.last_id = int(query.value(0))
2061 self.last_time = int(query.value(1))
2062 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
2063 if query.next():
2064 self.first_time = int(query.value(0))
2065 if placeholder_text:
2066 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
2067
2068 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
2069
2070 def IdBetween(self, query, lower_id, higher_id, order):
2071 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
2072 if query.next():
2073 return True, int(query.value(0))
2074 else:
2075 return False, 0
2076
2077 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
2078 query = QSqlQuery(self.glb.db)
2079 while True:
2080 next_id = int((lower_id + higher_id) / 2)
2081 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
2082 if not query.next():
2083 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
2084 if not ok:
2085 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
2086 if not ok:
2087 return str(higher_id)
2088 next_id = dbid
2089 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
2090 next_time = int(query.value(0))
2091 if get_floor:
2092 if target_time > next_time:
2093 lower_id = next_id
2094 else:
2095 higher_id = next_id
2096 if higher_id <= lower_id + 1:
2097 return str(higher_id)
2098 else:
2099 if target_time >= next_time:
2100 lower_id = next_id
2101 else:
2102 higher_id = next_id
2103 if higher_id <= lower_id + 1:
2104 return str(lower_id)
2105
2106 def ConvertRelativeTime(self, val):
2107 mult = 1
2108 suffix = val[-2:]
2109 if suffix == "ms":
2110 mult = 1000000
2111 elif suffix == "us":
2112 mult = 1000
2113 elif suffix == "ns":
2114 mult = 1
2115 else:
2116 return val
2117 val = val[:-2].strip()
2118 if not self.IsNumber(val):
2119 return val
2120 val = int(val) * mult
2121 if val >= 0:
2122 val += self.first_time
2123 else:
2124 val += self.last_time
2125 return str(val)
2126
2127 def ConvertTimeRange(self, vrange):
2128 if vrange[0] == "":
2129 vrange[0] = str(self.first_time)
2130 if vrange[1] == "":
2131 vrange[1] = str(self.last_time)
2132 vrange[0] = self.ConvertRelativeTime(vrange[0])
2133 vrange[1] = self.ConvertRelativeTime(vrange[1])
2134 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
2135 return False
2136 beg_range = max(int(vrange[0]), self.first_time)
2137 end_range = min(int(vrange[1]), self.last_time)
2138 if beg_range > self.last_time or end_range < self.first_time:
2139 return False
2140 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
2141 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
2142 return True
2143
2144 def AddTimeRange(self, value, ranges):
2145 n = value.count("-")
2146 if n == 1:
2147 pass
2148 elif n == 2:
2149 if value.split("-")[1].strip() == "":
2150 n = 1
2151 elif n == 3:
2152 n = 2
2153 else:
2154 return False
2155 pos = findnth(value, "-", n)
2156 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
2157 if self.ConvertTimeRange(vrange):
2158 ranges.append(vrange)
2159 return True
2160 return False
2161
2162 def DoValidate(self, input_string):
2163 ranges = []
2164 for value in [x.strip() for x in input_string.split(",")]:
2165 if not self.AddTimeRange(value, ranges):
2166 return self.InvalidValue(value)
2167 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
2168 self.value = " OR ".join(ranges)
2169
2170# Report Dialog Base
2171
2172class ReportDialogBase(QDialog):
2173
2174 def __init__(self, glb, title, items, partial, parent=None):
2175 super(ReportDialogBase, self).__init__(parent)
2176
2177 self.glb = glb
2178
2179 self.report_vars = ReportVars()
2180
2181 self.setWindowTitle(title)
2182 self.setMinimumWidth(600)
2183
2184 self.data_items = [x(glb, self) for x in items]
2185
2186 self.partial = partial
2187
2188 self.grid = QGridLayout()
2189
2190 for row in xrange(len(self.data_items)):
2191 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
2192 self.grid.addWidget(self.data_items[row].widget, row, 1)
2193
2194 self.status = QLabel()
2195
2196 self.ok_button = QPushButton("Ok", self)
2197 self.ok_button.setDefault(True)
2198 self.ok_button.released.connect(self.Ok)
2199 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2200
2201 self.cancel_button = QPushButton("Cancel", self)
2202 self.cancel_button.released.connect(self.reject)
2203 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2204
2205 self.hbox = QHBoxLayout()
2206 #self.hbox.addStretch()
2207 self.hbox.addWidget(self.status)
2208 self.hbox.addWidget(self.ok_button)
2209 self.hbox.addWidget(self.cancel_button)
2210
2211 self.vbox = QVBoxLayout()
2212 self.vbox.addLayout(self.grid)
2213 self.vbox.addLayout(self.hbox)
2214
2215 self.setLayout(self.vbox)
2216
2217 def Ok(self):
2218 vars = self.report_vars
2219 for d in self.data_items:
2220 if d.id == "REPORTNAME":
2221 vars.name = d.value
2222 if not vars.name:
2223 self.ShowMessage("Report name is required")
2224 return
2225 for d in self.data_items:
2226 if not d.IsValid():
2227 return
2228 for d in self.data_items[1:]:
2229 if d.id == "LIMIT":
2230 vars.limit = d.value
2231 elif len(d.value):
2232 if len(vars.where_clause):
2233 vars.where_clause += " AND "
2234 vars.where_clause += d.value
2235 if len(vars.where_clause):
2236 if self.partial:
2237 vars.where_clause = " AND ( " + vars.where_clause + " ) "
2238 else:
2239 vars.where_clause = " WHERE " + vars.where_clause + " "
2240 self.accept()
2241
2242 def ShowMessage(self, msg):
2243 self.status.setText("<font color=#FF0000>" + msg)
2244
2245 def ClearMessage(self):
2246 self.status.setText("")
2247
2248# Selected branch report creation dialog
2249
2250class SelectedBranchDialog(ReportDialogBase):
2251
2252 def __init__(self, glb, parent=None):
2253 title = "Selected Branches"
2254 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2255 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2256 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2257 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2258 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2259 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2260 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2261 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2262 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2263 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2264
2265# Event list
2266
2267def GetEventList(db):
2268 events = []
2269 query = QSqlQuery(db)
2270 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2271 while query.next():
2272 events.append(query.value(0))
2273 return events
2274
2275# Is a table selectable
2276
2277def IsSelectable(db, table, sql = "", columns = "*"):
2278 query = QSqlQuery(db)
2279 try:
2280 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
2281 except:
2282 return False
2283 return True
2284
2285# SQL table data model item
2286
2287class SQLTableItem():
2288
2289 def __init__(self, row, data):
2290 self.row = row
2291 self.data = data
2292
2293 def getData(self, column):
2294 return self.data[column]
2295
2296# SQL table data model
2297
2298class SQLTableModel(TableModel):
2299
2300 progress = Signal(object)
2301
2302 def __init__(self, glb, sql, column_headers, parent=None):
2303 super(SQLTableModel, self).__init__(parent)
2304 self.glb = glb
2305 self.more = True
2306 self.populated = 0
2307 self.column_headers = column_headers
2308 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
2309 self.fetcher.done.connect(self.Update)
2310 self.fetcher.Fetch(glb_chunk_sz)
2311
2312 def DisplayData(self, item, index):
2313 self.FetchIfNeeded(item.row)
2314 return item.getData(index.column())
2315
2316 def AddSample(self, data):
2317 child = SQLTableItem(self.populated, data)
2318 self.child_items.append(child)
2319 self.populated += 1
2320
2321 def Update(self, fetched):
2322 if not fetched:
2323 self.more = False
2324 self.progress.emit(0)
2325 child_count = self.child_count
2326 count = self.populated - child_count
2327 if count > 0:
2328 parent = QModelIndex()
2329 self.beginInsertRows(parent, child_count, child_count + count - 1)
2330 self.insertRows(child_count, count, parent)
2331 self.child_count += count
2332 self.endInsertRows()
2333 self.progress.emit(self.child_count)
2334
2335 def FetchMoreRecords(self, count):
2336 current = self.child_count
2337 if self.more:
2338 self.fetcher.Fetch(count)
2339 else:
2340 self.progress.emit(0)
2341 return current
2342
2343 def HasMoreRecords(self):
2344 return self.more
2345
2346 def columnCount(self, parent=None):
2347 return len(self.column_headers)
2348
2349 def columnHeader(self, column):
2350 return self.column_headers[column]
2351
2352 def SQLTableDataPrep(self, query, count):
2353 data = []
2354 for i in xrange(count):
2355 data.append(query.value(i))
2356 return data
2357
2358# SQL automatic table data model
2359
2360class SQLAutoTableModel(SQLTableModel):
2361
2362 def __init__(self, glb, table_name, parent=None):
2363 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2364 if table_name == "comm_threads_view":
2365 # For now, comm_threads_view has no id column
2366 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2367 column_headers = []
2368 query = QSqlQuery(glb.db)
2369 if glb.dbref.is_sqlite3:
2370 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2371 while query.next():
2372 column_headers.append(query.value(1))
2373 if table_name == "sqlite_master":
2374 sql = "SELECT * FROM " + table_name
2375 else:
2376 if table_name[:19] == "information_schema.":
2377 sql = "SELECT * FROM " + table_name
2378 select_table_name = table_name[19:]
2379 schema = "information_schema"
2380 else:
2381 select_table_name = table_name
2382 schema = "public"
2383 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2384 while query.next():
2385 column_headers.append(query.value(0))
2386 if pyside_version_1 and sys.version_info[0] == 3:
2387 if table_name == "samples_view":
2388 self.SQLTableDataPrep = self.samples_view_DataPrep
2389 if table_name == "samples":
2390 self.SQLTableDataPrep = self.samples_DataPrep
2391 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2392
2393 def samples_view_DataPrep(self, query, count):
2394 data = []
2395 data.append(query.value(0))
2396 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2397 data.append("{:>19}".format(query.value(1)))
2398 for i in xrange(2, count):
2399 data.append(query.value(i))
2400 return data
2401
2402 def samples_DataPrep(self, query, count):
2403 data = []
2404 for i in xrange(9):
2405 data.append(query.value(i))
2406 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2407 data.append("{:>19}".format(query.value(9)))
2408 for i in xrange(10, count):
2409 data.append(query.value(i))
2410 return data
2411
2412# Base class for custom ResizeColumnsToContents
2413
2414class ResizeColumnsToContentsBase(QObject):
2415
2416 def __init__(self, parent=None):
2417 super(ResizeColumnsToContentsBase, self).__init__(parent)
2418
2419 def ResizeColumnToContents(self, column, n):
2420 # Using the view's resizeColumnToContents() here is extrememly slow
2421 # so implement a crude alternative
2422 font = self.view.font()
2423 metrics = QFontMetrics(font)
2424 max = 0
2425 for row in xrange(n):
2426 val = self.data_model.child_items[row].data[column]
2427 len = metrics.width(str(val) + "MM")
2428 max = len if len > max else max
2429 val = self.data_model.columnHeader(column)
2430 len = metrics.width(str(val) + "MM")
2431 max = len if len > max else max
2432 self.view.setColumnWidth(column, max)
2433
2434 def ResizeColumnsToContents(self):
2435 n = min(self.data_model.child_count, 100)
2436 if n < 1:
2437 # No data yet, so connect a signal to notify when there is
2438 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2439 return
2440 columns = self.data_model.columnCount()
2441 for i in xrange(columns):
2442 self.ResizeColumnToContents(i, n)
2443
2444 def UpdateColumnWidths(self, *x):
2445 # This only needs to be done once, so disconnect the signal now
2446 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2447 self.ResizeColumnsToContents()
2448
2449# Convert value to CSV
2450
2451def ToCSValue(val):
2452 if '"' in val:
2453 val = val.replace('"', '""')
2454 if "," in val or '"' in val:
2455 val = '"' + val + '"'
2456 return val
2457
2458# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
2459
2460glb_max_cols = 1000
2461
2462def RowColumnKey(a):
2463 return a.row() * glb_max_cols + a.column()
2464
2465# Copy selected table cells to clipboard
2466
2467def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
2468 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
2469 idx_cnt = len(indexes)
2470 if not idx_cnt:
2471 return
2472 if idx_cnt == 1:
2473 with_hdr=False
2474 min_row = indexes[0].row()
2475 max_row = indexes[0].row()
2476 min_col = indexes[0].column()
2477 max_col = indexes[0].column()
2478 for i in indexes:
2479 min_row = min(min_row, i.row())
2480 max_row = max(max_row, i.row())
2481 min_col = min(min_col, i.column())
2482 max_col = max(max_col, i.column())
2483 if max_col > glb_max_cols:
2484 raise RuntimeError("glb_max_cols is too low")
2485 max_width = [0] * (1 + max_col - min_col)
2486 for i in indexes:
2487 c = i.column() - min_col
2488 max_width[c] = max(max_width[c], len(str(i.data())))
2489 text = ""
2490 pad = ""
2491 sep = ""
2492 if with_hdr:
2493 model = indexes[0].model()
2494 for col in range(min_col, max_col + 1):
2495 val = model.headerData(col, Qt.Horizontal)
2496 if as_csv:
2497 text += sep + ToCSValue(val)
2498 sep = ","
2499 else:
2500 c = col - min_col
2501 max_width[c] = max(max_width[c], len(val))
2502 width = max_width[c]
2503 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
2504 if align & Qt.AlignRight:
2505 val = val.rjust(width)
2506 text += pad + sep + val
2507 pad = " " * (width - len(val))
2508 sep = " "
2509 text += "\n"
2510 pad = ""
2511 sep = ""
2512 last_row = min_row
2513 for i in indexes:
2514 if i.row() > last_row:
2515 last_row = i.row()
2516 text += "\n"
2517 pad = ""
2518 sep = ""
2519 if as_csv:
2520 text += sep + ToCSValue(str(i.data()))
2521 sep = ","
2522 else:
2523 width = max_width[i.column() - min_col]
2524 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2525 val = str(i.data()).rjust(width)
2526 else:
2527 val = str(i.data())
2528 text += pad + sep + val
2529 pad = " " * (width - len(val))
2530 sep = " "
2531 QApplication.clipboard().setText(text)
2532
2533def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
2534 indexes = view.selectedIndexes()
2535 if not len(indexes):
2536 return
2537
2538 selection = view.selectionModel()
2539
2540 first = None
2541 for i in indexes:
2542 above = view.indexAbove(i)
2543 if not selection.isSelected(above):
2544 first = i
2545 break
2546
2547 if first is None:
2548 raise RuntimeError("CopyTreeCellsToClipboard internal error")
2549
2550 model = first.model()
2551 row_cnt = 0
2552 col_cnt = model.columnCount(first)
2553 max_width = [0] * col_cnt
2554
2555 indent_sz = 2
2556 indent_str = " " * indent_sz
2557
2558 expanded_mark_sz = 2
2559 if sys.version_info[0] == 3:
2560 expanded_mark = "\u25BC "
2561 not_expanded_mark = "\u25B6 "
2562 else:
2563 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
2564 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
2565 leaf_mark = " "
2566
2567 if not as_csv:
2568 pos = first
2569 while True:
2570 row_cnt += 1
2571 row = pos.row()
2572 for c in range(col_cnt):
2573 i = pos.sibling(row, c)
2574 if c:
2575 n = len(str(i.data()))
2576 else:
2577 n = len(str(i.data()).strip())
2578 n += (i.internalPointer().level - 1) * indent_sz
2579 n += expanded_mark_sz
2580 max_width[c] = max(max_width[c], n)
2581 pos = view.indexBelow(pos)
2582 if not selection.isSelected(pos):
2583 break
2584
2585 text = ""
2586 pad = ""
2587 sep = ""
2588 if with_hdr:
2589 for c in range(col_cnt):
2590 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
2591 if as_csv:
2592 text += sep + ToCSValue(val)
2593 sep = ","
2594 else:
2595 max_width[c] = max(max_width[c], len(val))
2596 width = max_width[c]
2597 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
2598 if align & Qt.AlignRight:
2599 val = val.rjust(width)
2600 text += pad + sep + val
2601 pad = " " * (width - len(val))
2602 sep = " "
2603 text += "\n"
2604 pad = ""
2605 sep = ""
2606
2607 pos = first
2608 while True:
2609 row = pos.row()
2610 for c in range(col_cnt):
2611 i = pos.sibling(row, c)
2612 val = str(i.data())
2613 if not c:
2614 if model.hasChildren(i):
2615 if view.isExpanded(i):
2616 mark = expanded_mark
2617 else:
2618 mark = not_expanded_mark
2619 else:
2620 mark = leaf_mark
2621 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
2622 if as_csv:
2623 text += sep + ToCSValue(val)
2624 sep = ","
2625 else:
2626 width = max_width[c]
2627 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
2628 val = val.rjust(width)
2629 text += pad + sep + val
2630 pad = " " * (width - len(val))
2631 sep = " "
2632 pos = view.indexBelow(pos)
2633 if not selection.isSelected(pos):
2634 break
2635 text = text.rstrip() + "\n"
2636 pad = ""
2637 sep = ""
2638
2639 QApplication.clipboard().setText(text)
2640
2641def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
2642 view.CopyCellsToClipboard(view, as_csv, with_hdr)
2643
2644def CopyCellsToClipboardHdr(view):
2645 CopyCellsToClipboard(view, False, True)
2646
2647def CopyCellsToClipboardCSV(view):
2648 CopyCellsToClipboard(view, True, True)
2649
2650# Context menu
2651
2652class ContextMenu(object):
2653
2654 def __init__(self, view):
2655 self.view = view
2656 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
2657 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
2658
2659 def ShowContextMenu(self, pos):
2660 menu = QMenu(self.view)
2661 self.AddActions(menu)
2662 menu.exec_(self.view.mapToGlobal(pos))
2663
2664 def AddCopy(self, menu):
2665 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
2666 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
2667
2668 def AddActions(self, menu):
2669 self.AddCopy(menu)
2670
2671class TreeContextMenu(ContextMenu):
2672
2673 def __init__(self, view):
2674 super(TreeContextMenu, self).__init__(view)
2675
2676 def AddActions(self, menu):
2677 i = self.view.currentIndex()
2678 text = str(i.data()).strip()
2679 if len(text):
2680 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
2681 self.AddCopy(menu)
2682
2683# Table window
2684
2685class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2686
2687 def __init__(self, glb, table_name, parent=None):
2688 super(TableWindow, self).__init__(parent)
2689
2690 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2691
2692 self.model = QSortFilterProxyModel()
2693 self.model.setSourceModel(self.data_model)
2694
2695 self.view = QTableView()
2696 self.view.setModel(self.model)
2697 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2698 self.view.verticalHeader().setVisible(False)
2699 self.view.sortByColumn(-1, Qt.AscendingOrder)
2700 self.view.setSortingEnabled(True)
2701 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2702 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
2703
2704 self.ResizeColumnsToContents()
2705
2706 self.context_menu = ContextMenu(self.view)
2707
2708 self.find_bar = FindBar(self, self, True)
2709
2710 self.finder = ChildDataItemFinder(self.data_model)
2711
2712 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2713
2714 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2715
2716 self.setWidget(self.vbox.Widget())
2717
2718 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2719
2720 def Find(self, value, direction, pattern, context):
2721 self.view.setFocus()
2722 self.find_bar.Busy()
2723 self.finder.Find(value, direction, pattern, context, self.FindDone)
2724
2725 def FindDone(self, row):
2726 self.find_bar.Idle()
2727 if row >= 0:
2728 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2729 else:
2730 self.find_bar.NotFound()
2731
2732# Table list
2733
2734def GetTableList(glb):
2735 tables = []
2736 query = QSqlQuery(glb.db)
2737 if glb.dbref.is_sqlite3:
2738 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2739 else:
2740 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2741 while query.next():
2742 tables.append(query.value(0))
2743 if glb.dbref.is_sqlite3:
2744 tables.append("sqlite_master")
2745 else:
2746 tables.append("information_schema.tables")
2747 tables.append("information_schema.views")
2748 tables.append("information_schema.columns")
2749 return tables
2750
2751# Top Calls data model
2752
2753class TopCallsModel(SQLTableModel):
2754
2755 def __init__(self, glb, report_vars, parent=None):
2756 text = ""
2757 if not glb.dbref.is_sqlite3:
2758 text = "::text"
2759 limit = ""
2760 if len(report_vars.limit):
2761 limit = " LIMIT " + report_vars.limit
2762 sql = ("SELECT comm, pid, tid, name,"
2763 " CASE"
2764 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2765 " ELSE short_name"
2766 " END AS dso,"
2767 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2768 " CASE"
2769 " WHEN (calls.flags = 1) THEN 'no call'" + text +
2770 " WHEN (calls.flags = 2) THEN 'no return'" + text +
2771 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2772 " ELSE ''" + text +
2773 " END AS flags"
2774 " FROM calls"
2775 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2776 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2777 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2778 " INNER JOIN comms ON calls.comm_id = comms.id"
2779 " INNER JOIN threads ON calls.thread_id = threads.id" +
2780 report_vars.where_clause +
2781 " ORDER BY elapsed_time DESC" +
2782 limit
2783 )
2784 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2785 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2786 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2787
2788 def columnAlignment(self, column):
2789 return self.alignment[column]
2790
2791# Top Calls report creation dialog
2792
2793class TopCallsDialog(ReportDialogBase):
2794
2795 def __init__(self, glb, parent=None):
2796 title = "Top Calls by Elapsed Time"
2797 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2798 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2799 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2800 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2801 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2802 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2803 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2804 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2805 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2806
2807# Top Calls window
2808
2809class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2810
2811 def __init__(self, glb, report_vars, parent=None):
2812 super(TopCallsWindow, self).__init__(parent)
2813
2814 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2815 self.model = self.data_model
2816
2817 self.view = QTableView()
2818 self.view.setModel(self.model)
2819 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2820 self.view.verticalHeader().setVisible(False)
2821 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
2822 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
2823
2824 self.context_menu = ContextMenu(self.view)
2825
2826 self.ResizeColumnsToContents()
2827
2828 self.find_bar = FindBar(self, self, True)
2829
2830 self.finder = ChildDataItemFinder(self.model)
2831
2832 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2833
2834 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2835
2836 self.setWidget(self.vbox.Widget())
2837
2838 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2839
2840 def Find(self, value, direction, pattern, context):
2841 self.view.setFocus()
2842 self.find_bar.Busy()
2843 self.finder.Find(value, direction, pattern, context, self.FindDone)
2844
2845 def FindDone(self, row):
2846 self.find_bar.Idle()
2847 if row >= 0:
2848 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2849 else:
2850 self.find_bar.NotFound()
2851
2852# Action Definition
2853
2854def CreateAction(label, tip, callback, parent=None, shortcut=None):
2855 action = QAction(label, parent)
2856 if shortcut != None:
2857 action.setShortcuts(shortcut)
2858 action.setStatusTip(tip)
2859 action.triggered.connect(callback)
2860 return action
2861
2862# Typical application actions
2863
2864def CreateExitAction(app, parent=None):
2865 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2866
2867# Typical MDI actions
2868
2869def CreateCloseActiveWindowAction(mdi_area):
2870 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2871
2872def CreateCloseAllWindowsAction(mdi_area):
2873 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2874
2875def CreateTileWindowsAction(mdi_area):
2876 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2877
2878def CreateCascadeWindowsAction(mdi_area):
2879 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2880
2881def CreateNextWindowAction(mdi_area):
2882 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2883
2884def CreatePreviousWindowAction(mdi_area):
2885 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2886
2887# Typical MDI window menu
2888
2889class WindowMenu():
2890
2891 def __init__(self, mdi_area, menu):
2892 self.mdi_area = mdi_area
2893 self.window_menu = menu.addMenu("&Windows")
2894 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2895 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2896 self.tile_windows = CreateTileWindowsAction(mdi_area)
2897 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2898 self.next_window = CreateNextWindowAction(mdi_area)
2899 self.previous_window = CreatePreviousWindowAction(mdi_area)
2900 self.window_menu.aboutToShow.connect(self.Update)
2901
2902 def Update(self):
2903 self.window_menu.clear()
2904 sub_window_count = len(self.mdi_area.subWindowList())
2905 have_sub_windows = sub_window_count != 0
2906 self.close_active_window.setEnabled(have_sub_windows)
2907 self.close_all_windows.setEnabled(have_sub_windows)
2908 self.tile_windows.setEnabled(have_sub_windows)
2909 self.cascade_windows.setEnabled(have_sub_windows)
2910 self.next_window.setEnabled(have_sub_windows)
2911 self.previous_window.setEnabled(have_sub_windows)
2912 self.window_menu.addAction(self.close_active_window)
2913 self.window_menu.addAction(self.close_all_windows)
2914 self.window_menu.addSeparator()
2915 self.window_menu.addAction(self.tile_windows)
2916 self.window_menu.addAction(self.cascade_windows)
2917 self.window_menu.addSeparator()
2918 self.window_menu.addAction(self.next_window)
2919 self.window_menu.addAction(self.previous_window)
2920 if sub_window_count == 0:
2921 return
2922 self.window_menu.addSeparator()
2923 nr = 1
2924 for sub_window in self.mdi_area.subWindowList():
2925 label = str(nr) + " " + sub_window.name
2926 if nr < 10:
2927 label = "&" + label
2928 action = self.window_menu.addAction(label)
2929 action.setCheckable(True)
2930 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2931 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
2932 self.window_menu.addAction(action)
2933 nr += 1
2934
2935 def setActiveSubWindow(self, nr):
2936 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2937
2938# Help text
2939
2940glb_help_text = """
2941<h1>Contents</h1>
2942<style>
2943p.c1 {
2944 text-indent: 40px;
2945}
2946p.c2 {
2947 text-indent: 80px;
2948}
2949}
2950</style>
2951<p class=c1><a href=#reports>1. Reports</a></p>
2952<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2953<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2954<p class=c2><a href=#allbranches>1.3 All branches</a></p>
2955<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2956<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2957<p class=c1><a href=#tables>2. Tables</a></p>
2958<h1 id=reports>1. Reports</h1>
2959<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2960The result is a GUI window with a tree representing a context-sensitive
2961call-graph. Expanding a couple of levels of the tree and adjusting column
2962widths to suit will display something like:
2963<pre>
2964 Call Graph: pt_example
2965Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
2966v- ls
2967 v- 2638:2638
2968 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
2969 |- unknown unknown 1 13198 0.1 1 0.0
2970 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
2971 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
2972 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
2973 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
2974 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
2975 >- __libc_csu_init ls 1 10354 0.1 10 0.0
2976 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
2977 v- main ls 1 8182043 99.6 180254 99.9
2978</pre>
2979<h3>Points to note:</h3>
2980<ul>
2981<li>The top level is a command name (comm)</li>
2982<li>The next level is a thread (pid:tid)</li>
2983<li>Subsequent levels are functions</li>
2984<li>'Count' is the number of calls</li>
2985<li>'Time' is the elapsed time until the function returns</li>
2986<li>Percentages are relative to the level above</li>
2987<li>'Branch Count' is the total number of branches for that function and all functions that it calls
2988</ul>
2989<h3>Find</h3>
2990Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2991The pattern matching symbols are ? for any character and * for zero or more characters.
2992<h2 id=calltree>1.2 Call Tree</h2>
2993The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2994Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2995<h2 id=allbranches>1.3 All branches</h2>
2996The All branches report displays all branches in chronological order.
2997Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2998<h3>Disassembly</h3>
2999Open a branch to display disassembly. This only works if:
3000<ol>
3001<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
3002<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
3003The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
3004One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
3005or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
3006</ol>
3007<h4 id=xed>Intel XED Setup</h4>
3008To use Intel XED, libxed.so must be present. To build and install libxed.so:
3009<pre>
3010git clone https://github.com/intelxed/mbuild.git mbuild
3011git clone https://github.com/intelxed/xed
3012cd xed
3013./mfile.py --share
3014sudo ./mfile.py --prefix=/usr/local install
3015sudo ldconfig
3016</pre>
3017<h3>Instructions per Cycle (IPC)</h3>
3018If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
3019<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
3020Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
3021In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
3022since the previous displayed 'IPC'.
3023<h3>Find</h3>
3024Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
3025Refer to Python documentation for the regular expression syntax.
3026All columns are searched, but only currently fetched rows are searched.
3027<h2 id=selectedbranches>1.4 Selected branches</h2>
3028This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
3029by various selection criteria. A dialog box displays available criteria which are AND'ed together.
3030<h3>1.4.1 Time ranges</h3>
3031The time ranges hint text shows the total time range. Relative time ranges can also be entered in
3032ms, us or ns. Also, negative values are relative to the end of trace. Examples:
3033<pre>
3034 81073085947329-81073085958238 From 81073085947329 to 81073085958238
3035 100us-200us From 100us to 200us
3036 10ms- From 10ms to the end
3037 -100ns The first 100ns
3038 -10ms- The last 10ms
3039</pre>
3040N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
3041<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
3042The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
3043The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
3044If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
3045<h1 id=tables>2. Tables</h1>
3046The Tables menu shows all tables and views in the database. Most tables have an associated view
3047which displays the information in a more friendly way. Not all data for large tables is fetched
3048immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
3049but that can be slow for large tables.
3050<p>There are also tables of database meta-information.
3051For SQLite3 databases, the sqlite_master table is included.
3052For PostgreSQL databases, information_schema.tables/views/columns are included.
3053<h3>Find</h3>
3054Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
3055Refer to Python documentation for the regular expression syntax.
3056All columns are searched, but only currently fetched rows are searched.
3057<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
3058will go to the next/previous result in id order, instead of display order.
3059"""
3060
3061# Help window
3062
3063class HelpWindow(QMdiSubWindow):
3064
3065 def __init__(self, glb, parent=None):
3066 super(HelpWindow, self).__init__(parent)
3067
3068 self.text = QTextBrowser()
3069 self.text.setHtml(glb_help_text)
3070 self.text.setReadOnly(True)
3071 self.text.setOpenExternalLinks(True)
3072
3073 self.setWidget(self.text)
3074
3075 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
3076
3077# Main window that only displays the help text
3078
3079class HelpOnlyWindow(QMainWindow):
3080
3081 def __init__(self, parent=None):
3082 super(HelpOnlyWindow, self).__init__(parent)
3083
3084 self.setMinimumSize(200, 100)
3085 self.resize(800, 600)
3086 self.setWindowTitle("Exported SQL Viewer Help")
3087 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
3088
3089 self.text = QTextBrowser()
3090 self.text.setHtml(glb_help_text)
3091 self.text.setReadOnly(True)
3092 self.text.setOpenExternalLinks(True)
3093
3094 self.setCentralWidget(self.text)
3095
3096# PostqreSQL server version
3097
3098def PostqreSQLServerVersion(db):
3099 query = QSqlQuery(db)
3100 QueryExec(query, "SELECT VERSION()")
3101 if query.next():
3102 v_str = query.value(0)
3103 v_list = v_str.strip().split(" ")
3104 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
3105 return v_list[1]
3106 return v_str
3107 return "Unknown"
3108
3109# SQLite version
3110
3111def SQLiteVersion(db):
3112 query = QSqlQuery(db)
3113 QueryExec(query, "SELECT sqlite_version()")
3114 if query.next():
3115 return query.value(0)
3116 return "Unknown"
3117
3118# About dialog
3119
3120class AboutDialog(QDialog):
3121
3122 def __init__(self, glb, parent=None):
3123 super(AboutDialog, self).__init__(parent)
3124
3125 self.setWindowTitle("About Exported SQL Viewer")
3126 self.setMinimumWidth(300)
3127
3128 pyside_version = "1" if pyside_version_1 else "2"
3129
3130 text = "<pre>"
3131 text += "Python version: " + sys.version.split(" ")[0] + "\n"
3132 text += "PySide version: " + pyside_version + "\n"
3133 text += "Qt version: " + qVersion() + "\n"
3134 if glb.dbref.is_sqlite3:
3135 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
3136 else:
3137 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
3138 text += "</pre>"
3139
3140 self.text = QTextBrowser()
3141 self.text.setHtml(text)
3142 self.text.setReadOnly(True)
3143 self.text.setOpenExternalLinks(True)
3144
3145 self.vbox = QVBoxLayout()
3146 self.vbox.addWidget(self.text)
3147
3148 self.setLayout(self.vbox)
3149
3150# Font resize
3151
3152def ResizeFont(widget, diff):
3153 font = widget.font()
3154 sz = font.pointSize()
3155 font.setPointSize(sz + diff)
3156 widget.setFont(font)
3157
3158def ShrinkFont(widget):
3159 ResizeFont(widget, -1)
3160
3161def EnlargeFont(widget):
3162 ResizeFont(widget, 1)
3163
3164# Unique name for sub-windows
3165
3166def NumberedWindowName(name, nr):
3167 if nr > 1:
3168 name += " <" + str(nr) + ">"
3169 return name
3170
3171def UniqueSubWindowName(mdi_area, name):
3172 nr = 1
3173 while True:
3174 unique_name = NumberedWindowName(name, nr)
3175 ok = True
3176 for sub_window in mdi_area.subWindowList():
3177 if sub_window.name == unique_name:
3178 ok = False
3179 break
3180 if ok:
3181 return unique_name
3182 nr += 1
3183
3184# Add a sub-window
3185
3186def AddSubWindow(mdi_area, sub_window, name):
3187 unique_name = UniqueSubWindowName(mdi_area, name)
3188 sub_window.setMinimumSize(200, 100)
3189 sub_window.resize(800, 600)
3190 sub_window.setWindowTitle(unique_name)
3191 sub_window.setAttribute(Qt.WA_DeleteOnClose)
3192 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
3193 sub_window.name = unique_name
3194 mdi_area.addSubWindow(sub_window)
3195 sub_window.show()
3196
3197# Main window
3198
3199class MainWindow(QMainWindow):
3200
3201 def __init__(self, glb, parent=None):
3202 super(MainWindow, self).__init__(parent)
3203
3204 self.glb = glb
3205
3206 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
3207 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
3208 self.setMinimumSize(200, 100)
3209
3210 self.mdi_area = QMdiArea()
3211 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
3212 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
3213
3214 self.setCentralWidget(self.mdi_area)
3215
3216 menu = self.menuBar()
3217
3218 file_menu = menu.addMenu("&File")
3219 file_menu.addAction(CreateExitAction(glb.app, self))
3220
3221 edit_menu = menu.addMenu("&Edit")
3222 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
3223 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
3224 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
3225 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
3226 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
3227 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
3228
3229 reports_menu = menu.addMenu("&Reports")
3230 if IsSelectable(glb.db, "calls"):
3231 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
3232
3233 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
3234 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
3235
3236 self.EventMenu(GetEventList(glb.db), reports_menu)
3237
3238 if IsSelectable(glb.db, "calls"):
3239 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
3240
3241 self.TableMenu(GetTableList(glb), menu)
3242
3243 self.window_menu = WindowMenu(self.mdi_area, menu)
3244
3245 help_menu = menu.addMenu("&Help")
3246 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
3247 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
3248
3249 def Try(self, fn):
3250 win = self.mdi_area.activeSubWindow()
3251 if win:
3252 try:
3253 fn(win.view)
3254 except:
3255 pass
3256
3257 def CopyToClipboard(self):
3258 self.Try(CopyCellsToClipboardHdr)
3259
3260 def CopyToClipboardCSV(self):
3261 self.Try(CopyCellsToClipboardCSV)
3262
3263 def Find(self):
3264 win = self.mdi_area.activeSubWindow()
3265 if win:
3266 try:
3267 win.find_bar.Activate()
3268 except:
3269 pass
3270
3271 def FetchMoreRecords(self):
3272 win = self.mdi_area.activeSubWindow()
3273 if win:
3274 try:
3275 win.fetch_bar.Activate()
3276 except:
3277 pass
3278
3279 def ShrinkFont(self):
3280 self.Try(ShrinkFont)
3281
3282 def EnlargeFont(self):
3283 self.Try(EnlargeFont)
3284
3285 def EventMenu(self, events, reports_menu):
3286 branches_events = 0
3287 for event in events:
3288 event = event.split(":")[0]
3289 if event == "branches":
3290 branches_events += 1
3291 dbid = 0
3292 for event in events:
3293 dbid += 1
3294 event = event.split(":")[0]
3295 if event == "branches":
3296 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
3297 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
3298 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
3299 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
3300
3301 def TableMenu(self, tables, menu):
3302 table_menu = menu.addMenu("&Tables")
3303 for table in tables:
3304 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
3305
3306 def NewCallGraph(self):
3307 CallGraphWindow(self.glb, self)
3308
3309 def NewCallTree(self):
3310 CallTreeWindow(self.glb, self)
3311
3312 def NewTopCalls(self):
3313 dialog = TopCallsDialog(self.glb, self)
3314 ret = dialog.exec_()
3315 if ret:
3316 TopCallsWindow(self.glb, dialog.report_vars, self)
3317
3318 def NewBranchView(self, event_id):
3319 BranchWindow(self.glb, event_id, ReportVars(), self)
3320
3321 def NewSelectedBranchView(self, event_id):
3322 dialog = SelectedBranchDialog(self.glb, self)
3323 ret = dialog.exec_()
3324 if ret:
3325 BranchWindow(self.glb, event_id, dialog.report_vars, self)
3326
3327 def NewTableView(self, table_name):
3328 TableWindow(self.glb, table_name, self)
3329
3330 def Help(self):
3331 HelpWindow(self.glb, self)
3332
3333 def About(self):
3334 dialog = AboutDialog(self.glb, self)
3335 dialog.exec_()
3336
3337# XED Disassembler
3338
3339class xed_state_t(Structure):
3340
3341 _fields_ = [
3342 ("mode", c_int),
3343 ("width", c_int)
3344 ]
3345
3346class XEDInstruction():
3347
3348 def __init__(self, libxed):
3349 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
3350 xedd_t = c_byte * 512
3351 self.xedd = xedd_t()
3352 self.xedp = addressof(self.xedd)
3353 libxed.xed_decoded_inst_zero(self.xedp)
3354 self.state = xed_state_t()
3355 self.statep = addressof(self.state)
3356 # Buffer for disassembled instruction text
3357 self.buffer = create_string_buffer(256)
3358 self.bufferp = addressof(self.buffer)
3359
3360class LibXED():
3361
3362 def __init__(self):
3363 try:
3364 self.libxed = CDLL("libxed.so")
3365 except:
3366 self.libxed = None
3367 if not self.libxed:
3368 self.libxed = CDLL("/usr/local/lib/libxed.so")
3369
3370 self.xed_tables_init = self.libxed.xed_tables_init
3371 self.xed_tables_init.restype = None
3372 self.xed_tables_init.argtypes = []
3373
3374 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
3375 self.xed_decoded_inst_zero.restype = None
3376 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
3377
3378 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
3379 self.xed_operand_values_set_mode.restype = None
3380 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
3381
3382 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
3383 self.xed_decoded_inst_zero_keep_mode.restype = None
3384 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
3385
3386 self.xed_decode = self.libxed.xed_decode
3387 self.xed_decode.restype = c_int
3388 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
3389
3390 self.xed_format_context = self.libxed.xed_format_context
3391 self.xed_format_context.restype = c_uint
3392 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
3393
3394 self.xed_tables_init()
3395
3396 def Instruction(self):
3397 return XEDInstruction(self)
3398
3399 def SetMode(self, inst, mode):
3400 if mode:
3401 inst.state.mode = 4 # 32-bit
3402 inst.state.width = 4 # 4 bytes
3403 else:
3404 inst.state.mode = 1 # 64-bit
3405 inst.state.width = 8 # 8 bytes
3406 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
3407
3408 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
3409 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
3410 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
3411 if err:
3412 return 0, ""
3413 # Use AT&T mode (2), alternative is Intel (3)
3414 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
3415 if not ok:
3416 return 0, ""
3417 if sys.version_info[0] == 2:
3418 result = inst.buffer.value
3419 else:
3420 result = inst.buffer.value.decode()
3421 # Return instruction length and the disassembled instruction text
3422 # For now, assume the length is in byte 166
3423 return inst.xedd[166], result
3424
3425def TryOpen(file_name):
3426 try:
3427 return open(file_name, "rb")
3428 except:
3429 return None
3430
3431def Is64Bit(f):
3432 result = sizeof(c_void_p)
3433 # ELF support only
3434 pos = f.tell()
3435 f.seek(0)
3436 header = f.read(7)
3437 f.seek(pos)
3438 magic = header[0:4]
3439 if sys.version_info[0] == 2:
3440 eclass = ord(header[4])
3441 encoding = ord(header[5])
3442 version = ord(header[6])
3443 else:
3444 eclass = header[4]
3445 encoding = header[5]
3446 version = header[6]
3447 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
3448 result = True if eclass == 2 else False
3449 return result
3450
3451# Global data
3452
3453class Glb():
3454
3455 def __init__(self, dbref, db, dbname):
3456 self.dbref = dbref
3457 self.db = db
3458 self.dbname = dbname
3459 self.home_dir = os.path.expanduser("~")
3460 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
3461 if self.buildid_dir:
3462 self.buildid_dir += "/.build-id/"
3463 else:
3464 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
3465 self.app = None
3466 self.mainwindow = None
3467 self.instances_to_shutdown_on_exit = weakref.WeakSet()
3468 try:
3469 self.disassembler = LibXED()
3470 self.have_disassembler = True
3471 except:
3472 self.have_disassembler = False
3473
3474 def FileFromBuildId(self, build_id):
3475 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
3476 return TryOpen(file_name)
3477
3478 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
3479 # Assume current machine i.e. no support for virtualization
3480 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
3481 file_name = os.getenv("PERF_KCORE")
3482 f = TryOpen(file_name) if file_name else None
3483 if f:
3484 return f
3485 # For now, no special handling if long_name is /proc/kcore
3486 f = TryOpen(long_name)
3487 if f:
3488 return f
3489 f = self.FileFromBuildId(build_id)
3490 if f:
3491 return f
3492 return None
3493
3494 def AddInstanceToShutdownOnExit(self, instance):
3495 self.instances_to_shutdown_on_exit.add(instance)
3496
3497 # Shutdown any background processes or threads
3498 def ShutdownInstances(self):
3499 for x in self.instances_to_shutdown_on_exit:
3500 try:
3501 x.Shutdown()
3502 except:
3503 pass
3504
3505# Database reference
3506
3507class DBRef():
3508
3509 def __init__(self, is_sqlite3, dbname):
3510 self.is_sqlite3 = is_sqlite3
3511 self.dbname = dbname
3512
3513 def Open(self, connection_name):
3514 dbname = self.dbname
3515 if self.is_sqlite3:
3516 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
3517 else:
3518 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
3519 opts = dbname.split()
3520 for opt in opts:
3521 if "=" in opt:
3522 opt = opt.split("=")
3523 if opt[0] == "hostname":
3524 db.setHostName(opt[1])
3525 elif opt[0] == "port":
3526 db.setPort(int(opt[1]))
3527 elif opt[0] == "username":
3528 db.setUserName(opt[1])
3529 elif opt[0] == "password":
3530 db.setPassword(opt[1])
3531 elif opt[0] == "dbname":
3532 dbname = opt[1]
3533 else:
3534 dbname = opt
3535
3536 db.setDatabaseName(dbname)
3537 if not db.open():
3538 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
3539 return db, dbname
3540
3541# Main
3542
3543def Main():
3544 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
3545 " or: exported-sql-viewer.py --help-only"
3546 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
3547 ap.add_argument("--pyside-version-1", action='store_true')
3548 ap.add_argument("dbname", nargs="?")
3549 ap.add_argument("--help-only", action='store_true')
3550 args = ap.parse_args()
3551
3552 if args.help_only:
3553 app = QApplication(sys.argv)
3554 mainwindow = HelpOnlyWindow()
3555 mainwindow.show()
3556 err = app.exec_()
3557 sys.exit(err)
3558
3559 dbname = args.dbname
3560 if dbname is None:
3561 ap.print_usage()
3562 print("Too few arguments")
3563 sys.exit(1)
3564
3565 is_sqlite3 = False
3566 try:
3567 f = open(dbname, "rb")
3568 if f.read(15) == b'SQLite format 3':
3569 is_sqlite3 = True
3570 f.close()
3571 except:
3572 pass
3573
3574 dbref = DBRef(is_sqlite3, dbname)
3575 db, dbname = dbref.Open("main")
3576 glb = Glb(dbref, db, dbname)
3577 app = QApplication(sys.argv)
3578 glb.app = app
3579 mainwindow = MainWindow(glb)
3580 glb.mainwindow = mainwindow
3581 mainwindow.show()
3582 err = app.exec_()
3583 glb.ShutdownInstances()
3584 db.close()
3585 sys.exit(err)
3586
3587if __name__ == "__main__":
3588 Main()
1#!/usr/bin/env python
2# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
5
6# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
8# scripts for details.
9#
10# Following on from the example in the export scripts, a
11# call-graph can be displayed for the pt_example database like this:
12#
13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14#
15# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
17#
18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph. Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24# Call Graph: pt_example
25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
26# v- ls
27# v- 2638:2638
28# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
29# |- unknown unknown 1 13198 0.1 1 0.0
30# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
31# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
32# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
33# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
34# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
35# >- __libc_csu_init ls 1 10354 0.1 10 0.0
36# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
37# v- main ls 1 8182043 99.6 180254 99.9
38#
39# Points to note:
40# The top level is a command name (comm)
41# The next level is a thread (pid:tid)
42# Subsequent levels are functions
43# 'Count' is the number of calls
44# 'Time' is the elapsed time until the function returns
45# Percentages are relative to the level above
46# 'Branch Count' is the total number of branches for that function and all
47# functions that it calls
48
49# There is also a "All branches" report, which displays branches and
50# possibly disassembly. However, presently, the only supported disassembler is
51# Intel XED, and additionally the object code must be present in perf build ID
52# cache. To use Intel XED, libxed.so must be present. To build and install
53# libxed.so:
54# git clone https://github.com/intelxed/mbuild.git mbuild
55# git clone https://github.com/intelxed/xed
56# cd xed
57# ./mfile.py --share
58# sudo ./mfile.py --prefix=/usr/local install
59# sudo ldconfig
60#
61# Example report:
62#
63# Time CPU Command PID TID Branch Type In Tx Branch
64# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65# 7fab593ea260 48 89 e7 mov %rsp, %rdi
66# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68# 7fab593ea260 48 89 e7 mov %rsp, %rdi
69# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
70# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71# 7fab593ea930 55 pushq %rbp
72# 7fab593ea931 48 89 e5 mov %rsp, %rbp
73# 7fab593ea934 41 57 pushq %r15
74# 7fab593ea936 41 56 pushq %r14
75# 7fab593ea938 41 55 pushq %r13
76# 7fab593ea93a 41 54 pushq %r12
77# 7fab593ea93c 53 pushq %rbx
78# 7fab593ea93d 48 89 fb mov %rdi, %rbx
79# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
80# 7fab593ea944 0f 31 rdtsc
81# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
82# 7fab593ea94a 89 c0 mov %eax, %eax
83# 7fab593ea94c 48 09 c2 or %rax, %rdx
84# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
85# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
88# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
89# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
91from __future__ import print_function
92
93import sys
94import argparse
95import weakref
96import threading
97import string
98try:
99 # Python2
100 import cPickle as pickle
101 # size of pickled integer big enough for record size
102 glb_nsz = 8
103except ImportError:
104 import pickle
105 glb_nsz = 16
106import re
107import os
108import random
109import copy
110import math
111
112pyside_version_1 = True
113if not "--pyside-version-1" in sys.argv:
114 try:
115 from PySide2.QtCore import *
116 from PySide2.QtGui import *
117 from PySide2.QtSql import *
118 from PySide2.QtWidgets import *
119 pyside_version_1 = False
120 except:
121 pass
122
123if pyside_version_1:
124 from PySide.QtCore import *
125 from PySide.QtGui import *
126 from PySide.QtSql import *
127
128from decimal import *
129from ctypes import *
130from multiprocessing import Process, Array, Value, Event
131
132# xrange is range in Python3
133try:
134 xrange
135except NameError:
136 xrange = range
137
138def printerr(*args, **keyword_args):
139 print(*args, file=sys.stderr, **keyword_args)
140
141# Data formatting helpers
142
143def tohex(ip):
144 if ip < 0:
145 ip += 1 << 64
146 return "%x" % ip
147
148def offstr(offset):
149 if offset:
150 return "+0x%x" % offset
151 return ""
152
153def dsoname(name):
154 if name == "[kernel.kallsyms]":
155 return "[kernel]"
156 return name
157
158def findnth(s, sub, n, offs=0):
159 pos = s.find(sub)
160 if pos < 0:
161 return pos
162 if n <= 1:
163 return offs + pos
164 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
165
166# Percent to one decimal place
167
168def PercentToOneDP(n, d):
169 if not d:
170 return "0.0"
171 x = (n * Decimal(100)) / d
172 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
173
174# Helper for queries that must not fail
175
176def QueryExec(query, stmt):
177 ret = query.exec_(stmt)
178 if not ret:
179 raise Exception("Query failed: " + query.lastError().text())
180
181# Background thread
182
183class Thread(QThread):
184
185 done = Signal(object)
186
187 def __init__(self, task, param=None, parent=None):
188 super(Thread, self).__init__(parent)
189 self.task = task
190 self.param = param
191
192 def run(self):
193 while True:
194 if self.param is None:
195 done, result = self.task()
196 else:
197 done, result = self.task(self.param)
198 self.done.emit(result)
199 if done:
200 break
201
202# Tree data model
203
204class TreeModel(QAbstractItemModel):
205
206 def __init__(self, glb, params, parent=None):
207 super(TreeModel, self).__init__(parent)
208 self.glb = glb
209 self.params = params
210 self.root = self.GetRoot()
211 self.last_row_read = 0
212
213 def Item(self, parent):
214 if parent.isValid():
215 return parent.internalPointer()
216 else:
217 return self.root
218
219 def rowCount(self, parent):
220 result = self.Item(parent).childCount()
221 if result < 0:
222 result = 0
223 self.dataChanged.emit(parent, parent)
224 return result
225
226 def hasChildren(self, parent):
227 return self.Item(parent).hasChildren()
228
229 def headerData(self, section, orientation, role):
230 if role == Qt.TextAlignmentRole:
231 return self.columnAlignment(section)
232 if role != Qt.DisplayRole:
233 return None
234 if orientation != Qt.Horizontal:
235 return None
236 return self.columnHeader(section)
237
238 def parent(self, child):
239 child_item = child.internalPointer()
240 if child_item is self.root:
241 return QModelIndex()
242 parent_item = child_item.getParentItem()
243 return self.createIndex(parent_item.getRow(), 0, parent_item)
244
245 def index(self, row, column, parent):
246 child_item = self.Item(parent).getChildItem(row)
247 return self.createIndex(row, column, child_item)
248
249 def DisplayData(self, item, index):
250 return item.getData(index.column())
251
252 def FetchIfNeeded(self, row):
253 if row > self.last_row_read:
254 self.last_row_read = row
255 if row + 10 >= self.root.child_count:
256 self.fetcher.Fetch(glb_chunk_sz)
257
258 def columnAlignment(self, column):
259 return Qt.AlignLeft
260
261 def columnFont(self, column):
262 return None
263
264 def data(self, index, role):
265 if role == Qt.TextAlignmentRole:
266 return self.columnAlignment(index.column())
267 if role == Qt.FontRole:
268 return self.columnFont(index.column())
269 if role != Qt.DisplayRole:
270 return None
271 item = index.internalPointer()
272 return self.DisplayData(item, index)
273
274# Table data model
275
276class TableModel(QAbstractTableModel):
277
278 def __init__(self, parent=None):
279 super(TableModel, self).__init__(parent)
280 self.child_count = 0
281 self.child_items = []
282 self.last_row_read = 0
283
284 def Item(self, parent):
285 if parent.isValid():
286 return parent.internalPointer()
287 else:
288 return self
289
290 def rowCount(self, parent):
291 return self.child_count
292
293 def headerData(self, section, orientation, role):
294 if role == Qt.TextAlignmentRole:
295 return self.columnAlignment(section)
296 if role != Qt.DisplayRole:
297 return None
298 if orientation != Qt.Horizontal:
299 return None
300 return self.columnHeader(section)
301
302 def index(self, row, column, parent):
303 return self.createIndex(row, column, self.child_items[row])
304
305 def DisplayData(self, item, index):
306 return item.getData(index.column())
307
308 def FetchIfNeeded(self, row):
309 if row > self.last_row_read:
310 self.last_row_read = row
311 if row + 10 >= self.child_count:
312 self.fetcher.Fetch(glb_chunk_sz)
313
314 def columnAlignment(self, column):
315 return Qt.AlignLeft
316
317 def columnFont(self, column):
318 return None
319
320 def data(self, index, role):
321 if role == Qt.TextAlignmentRole:
322 return self.columnAlignment(index.column())
323 if role == Qt.FontRole:
324 return self.columnFont(index.column())
325 if role != Qt.DisplayRole:
326 return None
327 item = index.internalPointer()
328 return self.DisplayData(item, index)
329
330# Model cache
331
332model_cache = weakref.WeakValueDictionary()
333model_cache_lock = threading.Lock()
334
335def LookupCreateModel(model_name, create_fn):
336 model_cache_lock.acquire()
337 try:
338 model = model_cache[model_name]
339 except:
340 model = None
341 if model is None:
342 model = create_fn()
343 model_cache[model_name] = model
344 model_cache_lock.release()
345 return model
346
347def LookupModel(model_name):
348 model_cache_lock.acquire()
349 try:
350 model = model_cache[model_name]
351 except:
352 model = None
353 model_cache_lock.release()
354 return model
355
356# Find bar
357
358class FindBar():
359
360 def __init__(self, parent, finder, is_reg_expr=False):
361 self.finder = finder
362 self.context = []
363 self.last_value = None
364 self.last_pattern = None
365
366 label = QLabel("Find:")
367 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
368
369 self.textbox = QComboBox()
370 self.textbox.setEditable(True)
371 self.textbox.currentIndexChanged.connect(self.ValueChanged)
372
373 self.progress = QProgressBar()
374 self.progress.setRange(0, 0)
375 self.progress.hide()
376
377 if is_reg_expr:
378 self.pattern = QCheckBox("Regular Expression")
379 else:
380 self.pattern = QCheckBox("Pattern")
381 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
382
383 self.next_button = QToolButton()
384 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
385 self.next_button.released.connect(lambda: self.NextPrev(1))
386
387 self.prev_button = QToolButton()
388 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
389 self.prev_button.released.connect(lambda: self.NextPrev(-1))
390
391 self.close_button = QToolButton()
392 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
393 self.close_button.released.connect(self.Deactivate)
394
395 self.hbox = QHBoxLayout()
396 self.hbox.setContentsMargins(0, 0, 0, 0)
397
398 self.hbox.addWidget(label)
399 self.hbox.addWidget(self.textbox)
400 self.hbox.addWidget(self.progress)
401 self.hbox.addWidget(self.pattern)
402 self.hbox.addWidget(self.next_button)
403 self.hbox.addWidget(self.prev_button)
404 self.hbox.addWidget(self.close_button)
405
406 self.bar = QWidget()
407 self.bar.setLayout(self.hbox)
408 self.bar.hide()
409
410 def Widget(self):
411 return self.bar
412
413 def Activate(self):
414 self.bar.show()
415 self.textbox.lineEdit().selectAll()
416 self.textbox.setFocus()
417
418 def Deactivate(self):
419 self.bar.hide()
420
421 def Busy(self):
422 self.textbox.setEnabled(False)
423 self.pattern.hide()
424 self.next_button.hide()
425 self.prev_button.hide()
426 self.progress.show()
427
428 def Idle(self):
429 self.textbox.setEnabled(True)
430 self.progress.hide()
431 self.pattern.show()
432 self.next_button.show()
433 self.prev_button.show()
434
435 def Find(self, direction):
436 value = self.textbox.currentText()
437 pattern = self.pattern.isChecked()
438 self.last_value = value
439 self.last_pattern = pattern
440 self.finder.Find(value, direction, pattern, self.context)
441
442 def ValueChanged(self):
443 value = self.textbox.currentText()
444 pattern = self.pattern.isChecked()
445 index = self.textbox.currentIndex()
446 data = self.textbox.itemData(index)
447 # Store the pattern in the combo box to keep it with the text value
448 if data == None:
449 self.textbox.setItemData(index, pattern)
450 else:
451 self.pattern.setChecked(data)
452 self.Find(0)
453
454 def NextPrev(self, direction):
455 value = self.textbox.currentText()
456 pattern = self.pattern.isChecked()
457 if value != self.last_value:
458 index = self.textbox.findText(value)
459 # Allow for a button press before the value has been added to the combo box
460 if index < 0:
461 index = self.textbox.count()
462 self.textbox.addItem(value, pattern)
463 self.textbox.setCurrentIndex(index)
464 return
465 else:
466 self.textbox.setItemData(index, pattern)
467 elif pattern != self.last_pattern:
468 # Keep the pattern recorded in the combo box up to date
469 index = self.textbox.currentIndex()
470 self.textbox.setItemData(index, pattern)
471 self.Find(direction)
472
473 def NotFound(self):
474 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
475
476# Context-sensitive call graph data model item base
477
478class CallGraphLevelItemBase(object):
479
480 def __init__(self, glb, params, row, parent_item):
481 self.glb = glb
482 self.params = params
483 self.row = row
484 self.parent_item = parent_item
485 self.query_done = False
486 self.child_count = 0
487 self.child_items = []
488 if parent_item:
489 self.level = parent_item.level + 1
490 else:
491 self.level = 0
492
493 def getChildItem(self, row):
494 return self.child_items[row]
495
496 def getParentItem(self):
497 return self.parent_item
498
499 def getRow(self):
500 return self.row
501
502 def childCount(self):
503 if not self.query_done:
504 self.Select()
505 if not self.child_count:
506 return -1
507 return self.child_count
508
509 def hasChildren(self):
510 if not self.query_done:
511 return True
512 return self.child_count > 0
513
514 def getData(self, column):
515 return self.data[column]
516
517# Context-sensitive call graph data model level 2+ item base
518
519class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
520
521 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
522 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
523 self.comm_id = comm_id
524 self.thread_id = thread_id
525 self.call_path_id = call_path_id
526 self.insn_cnt = insn_cnt
527 self.cyc_cnt = cyc_cnt
528 self.branch_count = branch_count
529 self.time = time
530
531 def Select(self):
532 self.query_done = True
533 query = QSqlQuery(self.glb.db)
534 if self.params.have_ipc:
535 ipc_str = ", SUM(insn_count), SUM(cyc_count)"
536 else:
537 ipc_str = ""
538 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
539 " FROM calls"
540 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
541 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
542 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
543 " WHERE parent_call_path_id = " + str(self.call_path_id) +
544 " AND comm_id = " + str(self.comm_id) +
545 " AND thread_id = " + str(self.thread_id) +
546 " GROUP BY call_path_id, name, short_name"
547 " ORDER BY call_path_id")
548 while query.next():
549 if self.params.have_ipc:
550 insn_cnt = int(query.value(5))
551 cyc_cnt = int(query.value(6))
552 branch_count = int(query.value(7))
553 else:
554 insn_cnt = 0
555 cyc_cnt = 0
556 branch_count = int(query.value(5))
557 child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
558 self.child_items.append(child_item)
559 self.child_count += 1
560
561# Context-sensitive call graph data model level three item
562
563class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
564
565 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
566 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
567 dso = dsoname(dso)
568 if self.params.have_ipc:
569 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
570 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
571 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
572 ipc = CalcIPC(cyc_cnt, insn_cnt)
573 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
574 else:
575 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
576 self.dbid = call_path_id
577
578# Context-sensitive call graph data model level two item
579
580class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
581
582 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
583 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
584 if self.params.have_ipc:
585 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
586 else:
587 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
588 self.dbid = thread_id
589
590 def Select(self):
591 super(CallGraphLevelTwoItem, self).Select()
592 for child_item in self.child_items:
593 self.time += child_item.time
594 self.insn_cnt += child_item.insn_cnt
595 self.cyc_cnt += child_item.cyc_cnt
596 self.branch_count += child_item.branch_count
597 for child_item in self.child_items:
598 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
599 if self.params.have_ipc:
600 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
601 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
602 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
603 else:
604 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
605
606# Context-sensitive call graph data model level one item
607
608class CallGraphLevelOneItem(CallGraphLevelItemBase):
609
610 def __init__(self, glb, params, row, comm_id, comm, parent_item):
611 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
612 if self.params.have_ipc:
613 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
614 else:
615 self.data = [comm, "", "", "", "", "", ""]
616 self.dbid = comm_id
617
618 def Select(self):
619 self.query_done = True
620 query = QSqlQuery(self.glb.db)
621 QueryExec(query, "SELECT thread_id, pid, tid"
622 " FROM comm_threads"
623 " INNER JOIN threads ON thread_id = threads.id"
624 " WHERE comm_id = " + str(self.dbid))
625 while query.next():
626 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
627 self.child_items.append(child_item)
628 self.child_count += 1
629
630# Context-sensitive call graph data model root item
631
632class CallGraphRootItem(CallGraphLevelItemBase):
633
634 def __init__(self, glb, params):
635 super(CallGraphRootItem, self).__init__(glb, params, 0, None)
636 self.dbid = 0
637 self.query_done = True
638 if_has_calls = ""
639 if IsSelectable(glb.db, "comms", columns = "has_calls"):
640 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
641 query = QSqlQuery(glb.db)
642 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
643 while query.next():
644 if not query.value(0):
645 continue
646 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
647 self.child_items.append(child_item)
648 self.child_count += 1
649
650# Call graph model parameters
651
652class CallGraphModelParams():
653
654 def __init__(self, glb, parent=None):
655 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
656
657# Context-sensitive call graph data model base
658
659class CallGraphModelBase(TreeModel):
660
661 def __init__(self, glb, parent=None):
662 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
663
664 def FindSelect(self, value, pattern, query):
665 if pattern:
666 # postgresql and sqlite pattern patching differences:
667 # postgresql LIKE is case sensitive but sqlite LIKE is not
668 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
669 # postgresql supports ILIKE which is case insensitive
670 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
671 if not self.glb.dbref.is_sqlite3:
672 # Escape % and _
673 s = value.replace("%", "\%")
674 s = s.replace("_", "\_")
675 # Translate * and ? into SQL LIKE pattern characters % and _
676 trans = string.maketrans("*?", "%_")
677 match = " LIKE '" + str(s).translate(trans) + "'"
678 else:
679 match = " GLOB '" + str(value) + "'"
680 else:
681 match = " = '" + str(value) + "'"
682 self.DoFindSelect(query, match)
683
684 def Found(self, query, found):
685 if found:
686 return self.FindPath(query)
687 return []
688
689 def FindValue(self, value, pattern, query, last_value, last_pattern):
690 if last_value == value and pattern == last_pattern:
691 found = query.first()
692 else:
693 self.FindSelect(value, pattern, query)
694 found = query.next()
695 return self.Found(query, found)
696
697 def FindNext(self, query):
698 found = query.next()
699 if not found:
700 found = query.first()
701 return self.Found(query, found)
702
703 def FindPrev(self, query):
704 found = query.previous()
705 if not found:
706 found = query.last()
707 return self.Found(query, found)
708
709 def FindThread(self, c):
710 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
711 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
712 elif c.direction > 0:
713 ids = self.FindNext(c.query)
714 else:
715 ids = self.FindPrev(c.query)
716 return (True, ids)
717
718 def Find(self, value, direction, pattern, context, callback):
719 class Context():
720 def __init__(self, *x):
721 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
722 def Update(self, *x):
723 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
724 if len(context):
725 context[0].Update(value, direction, pattern)
726 else:
727 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
728 # Use a thread so the UI is not blocked during the SELECT
729 thread = Thread(self.FindThread, context[0])
730 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
731 thread.start()
732
733 def FindDone(self, thread, callback, ids):
734 callback(ids)
735
736# Context-sensitive call graph data model
737
738class CallGraphModel(CallGraphModelBase):
739
740 def __init__(self, glb, parent=None):
741 super(CallGraphModel, self).__init__(glb, parent)
742
743 def GetRoot(self):
744 return CallGraphRootItem(self.glb, self.params)
745
746 def columnCount(self, parent=None):
747 if self.params.have_ipc:
748 return 12
749 else:
750 return 7
751
752 def columnHeader(self, column):
753 if self.params.have_ipc:
754 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
755 else:
756 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
757 return headers[column]
758
759 def columnAlignment(self, column):
760 if self.params.have_ipc:
761 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
762 else:
763 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
764 return alignment[column]
765
766 def DoFindSelect(self, query, match):
767 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
768 " FROM calls"
769 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
770 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
771 " WHERE calls.id <> 0"
772 " AND symbols.name" + match +
773 " GROUP BY comm_id, thread_id, call_path_id"
774 " ORDER BY comm_id, thread_id, call_path_id")
775
776 def FindPath(self, query):
777 # Turn the query result into a list of ids that the tree view can walk
778 # to open the tree at the right place.
779 ids = []
780 parent_id = query.value(0)
781 while parent_id:
782 ids.insert(0, parent_id)
783 q2 = QSqlQuery(self.glb.db)
784 QueryExec(q2, "SELECT parent_id"
785 " FROM call_paths"
786 " WHERE id = " + str(parent_id))
787 if not q2.next():
788 break
789 parent_id = q2.value(0)
790 # The call path root is not used
791 if ids[0] == 1:
792 del ids[0]
793 ids.insert(0, query.value(2))
794 ids.insert(0, query.value(1))
795 return ids
796
797# Call tree data model level 2+ item base
798
799class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
800
801 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
802 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
803 self.comm_id = comm_id
804 self.thread_id = thread_id
805 self.calls_id = calls_id
806 self.call_time = call_time
807 self.time = time
808 self.insn_cnt = insn_cnt
809 self.cyc_cnt = cyc_cnt
810 self.branch_count = branch_count
811
812 def Select(self):
813 self.query_done = True
814 if self.calls_id == 0:
815 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
816 else:
817 comm_thread = ""
818 if self.params.have_ipc:
819 ipc_str = ", insn_count, cyc_count"
820 else:
821 ipc_str = ""
822 query = QSqlQuery(self.glb.db)
823 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
824 " FROM calls"
825 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
826 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
827 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
828 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
829 " ORDER BY call_time, calls.id")
830 while query.next():
831 if self.params.have_ipc:
832 insn_cnt = int(query.value(5))
833 cyc_cnt = int(query.value(6))
834 branch_count = int(query.value(7))
835 else:
836 insn_cnt = 0
837 cyc_cnt = 0
838 branch_count = int(query.value(5))
839 child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
840 self.child_items.append(child_item)
841 self.child_count += 1
842
843# Call tree data model level three item
844
845class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
846
847 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
848 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
849 dso = dsoname(dso)
850 if self.params.have_ipc:
851 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
852 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
853 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
854 ipc = CalcIPC(cyc_cnt, insn_cnt)
855 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
856 else:
857 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
858 self.dbid = calls_id
859
860# Call tree data model level two item
861
862class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
863
864 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
865 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
866 if self.params.have_ipc:
867 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
868 else:
869 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
870 self.dbid = thread_id
871
872 def Select(self):
873 super(CallTreeLevelTwoItem, self).Select()
874 for child_item in self.child_items:
875 self.time += child_item.time
876 self.insn_cnt += child_item.insn_cnt
877 self.cyc_cnt += child_item.cyc_cnt
878 self.branch_count += child_item.branch_count
879 for child_item in self.child_items:
880 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
881 if self.params.have_ipc:
882 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
883 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
884 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
885 else:
886 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
887
888# Call tree data model level one item
889
890class CallTreeLevelOneItem(CallGraphLevelItemBase):
891
892 def __init__(self, glb, params, row, comm_id, comm, parent_item):
893 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
894 if self.params.have_ipc:
895 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
896 else:
897 self.data = [comm, "", "", "", "", "", ""]
898 self.dbid = comm_id
899
900 def Select(self):
901 self.query_done = True
902 query = QSqlQuery(self.glb.db)
903 QueryExec(query, "SELECT thread_id, pid, tid"
904 " FROM comm_threads"
905 " INNER JOIN threads ON thread_id = threads.id"
906 " WHERE comm_id = " + str(self.dbid))
907 while query.next():
908 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
909 self.child_items.append(child_item)
910 self.child_count += 1
911
912# Call tree data model root item
913
914class CallTreeRootItem(CallGraphLevelItemBase):
915
916 def __init__(self, glb, params):
917 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
918 self.dbid = 0
919 self.query_done = True
920 if_has_calls = ""
921 if IsSelectable(glb.db, "comms", columns = "has_calls"):
922 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
923 query = QSqlQuery(glb.db)
924 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
925 while query.next():
926 if not query.value(0):
927 continue
928 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
929 self.child_items.append(child_item)
930 self.child_count += 1
931
932# Call Tree data model
933
934class CallTreeModel(CallGraphModelBase):
935
936 def __init__(self, glb, parent=None):
937 super(CallTreeModel, self).__init__(glb, parent)
938
939 def GetRoot(self):
940 return CallTreeRootItem(self.glb, self.params)
941
942 def columnCount(self, parent=None):
943 if self.params.have_ipc:
944 return 12
945 else:
946 return 7
947
948 def columnHeader(self, column):
949 if self.params.have_ipc:
950 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
951 else:
952 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
953 return headers[column]
954
955 def columnAlignment(self, column):
956 if self.params.have_ipc:
957 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
958 else:
959 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
960 return alignment[column]
961
962 def DoFindSelect(self, query, match):
963 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
964 " FROM calls"
965 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
966 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
967 " WHERE calls.id <> 0"
968 " AND symbols.name" + match +
969 " ORDER BY comm_id, thread_id, call_time, calls.id")
970
971 def FindPath(self, query):
972 # Turn the query result into a list of ids that the tree view can walk
973 # to open the tree at the right place.
974 ids = []
975 parent_id = query.value(0)
976 while parent_id:
977 ids.insert(0, parent_id)
978 q2 = QSqlQuery(self.glb.db)
979 QueryExec(q2, "SELECT parent_id"
980 " FROM calls"
981 " WHERE id = " + str(parent_id))
982 if not q2.next():
983 break
984 parent_id = q2.value(0)
985 ids.insert(0, query.value(2))
986 ids.insert(0, query.value(1))
987 return ids
988
989# Vertical layout
990
991class HBoxLayout(QHBoxLayout):
992
993 def __init__(self, *children):
994 super(HBoxLayout, self).__init__()
995
996 self.layout().setContentsMargins(0, 0, 0, 0)
997 for child in children:
998 if child.isWidgetType():
999 self.layout().addWidget(child)
1000 else:
1001 self.layout().addLayout(child)
1002
1003# Horizontal layout
1004
1005class VBoxLayout(QVBoxLayout):
1006
1007 def __init__(self, *children):
1008 super(VBoxLayout, self).__init__()
1009
1010 self.layout().setContentsMargins(0, 0, 0, 0)
1011 for child in children:
1012 if child.isWidgetType():
1013 self.layout().addWidget(child)
1014 else:
1015 self.layout().addLayout(child)
1016
1017# Vertical layout widget
1018
1019class VBox():
1020
1021 def __init__(self, *children):
1022 self.vbox = QWidget()
1023 self.vbox.setLayout(VBoxLayout(*children))
1024
1025 def Widget(self):
1026 return self.vbox
1027
1028# Tree window base
1029
1030class TreeWindowBase(QMdiSubWindow):
1031
1032 def __init__(self, parent=None):
1033 super(TreeWindowBase, self).__init__(parent)
1034
1035 self.model = None
1036 self.find_bar = None
1037
1038 self.view = QTreeView()
1039 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1040 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1041
1042 self.context_menu = TreeContextMenu(self.view)
1043
1044 def DisplayFound(self, ids):
1045 if not len(ids):
1046 return False
1047 parent = QModelIndex()
1048 for dbid in ids:
1049 found = False
1050 n = self.model.rowCount(parent)
1051 for row in xrange(n):
1052 child = self.model.index(row, 0, parent)
1053 if child.internalPointer().dbid == dbid:
1054 found = True
1055 self.view.setExpanded(parent, True)
1056 self.view.setCurrentIndex(child)
1057 parent = child
1058 break
1059 if not found:
1060 break
1061 return found
1062
1063 def Find(self, value, direction, pattern, context):
1064 self.view.setFocus()
1065 self.find_bar.Busy()
1066 self.model.Find(value, direction, pattern, context, self.FindDone)
1067
1068 def FindDone(self, ids):
1069 found = True
1070 if not self.DisplayFound(ids):
1071 found = False
1072 self.find_bar.Idle()
1073 if not found:
1074 self.find_bar.NotFound()
1075
1076
1077# Context-sensitive call graph window
1078
1079class CallGraphWindow(TreeWindowBase):
1080
1081 def __init__(self, glb, parent=None):
1082 super(CallGraphWindow, self).__init__(parent)
1083
1084 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1085
1086 self.view.setModel(self.model)
1087
1088 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1089 self.view.setColumnWidth(c, w)
1090
1091 self.find_bar = FindBar(self, self)
1092
1093 self.vbox = VBox(self.view, self.find_bar.Widget())
1094
1095 self.setWidget(self.vbox.Widget())
1096
1097 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1098
1099# Call tree window
1100
1101class CallTreeWindow(TreeWindowBase):
1102
1103 def __init__(self, glb, parent=None, thread_at_time=None):
1104 super(CallTreeWindow, self).__init__(parent)
1105
1106 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1107
1108 self.view.setModel(self.model)
1109
1110 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1111 self.view.setColumnWidth(c, w)
1112
1113 self.find_bar = FindBar(self, self)
1114
1115 self.vbox = VBox(self.view, self.find_bar.Widget())
1116
1117 self.setWidget(self.vbox.Widget())
1118
1119 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1120
1121 if thread_at_time:
1122 self.DisplayThreadAtTime(*thread_at_time)
1123
1124 def DisplayThreadAtTime(self, comm_id, thread_id, time):
1125 parent = QModelIndex()
1126 for dbid in (comm_id, thread_id):
1127 found = False
1128 n = self.model.rowCount(parent)
1129 for row in xrange(n):
1130 child = self.model.index(row, 0, parent)
1131 if child.internalPointer().dbid == dbid:
1132 found = True
1133 self.view.setExpanded(parent, True)
1134 self.view.setCurrentIndex(child)
1135 parent = child
1136 break
1137 if not found:
1138 return
1139 found = False
1140 while True:
1141 n = self.model.rowCount(parent)
1142 if not n:
1143 return
1144 last_child = None
1145 for row in xrange(n):
1146 self.view.setExpanded(parent, True)
1147 child = self.model.index(row, 0, parent)
1148 child_call_time = child.internalPointer().call_time
1149 if child_call_time < time:
1150 last_child = child
1151 elif child_call_time == time:
1152 self.view.setCurrentIndex(child)
1153 return
1154 elif child_call_time > time:
1155 break
1156 if not last_child:
1157 if not found:
1158 child = self.model.index(0, 0, parent)
1159 self.view.setExpanded(parent, True)
1160 self.view.setCurrentIndex(child)
1161 return
1162 found = True
1163 self.view.setExpanded(parent, True)
1164 self.view.setCurrentIndex(last_child)
1165 parent = last_child
1166
1167# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1168
1169def ExecComm(db, thread_id, time):
1170 query = QSqlQuery(db)
1171 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1172 " FROM comm_threads"
1173 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1174 " WHERE comm_threads.thread_id = " + str(thread_id) +
1175 " ORDER BY comms.c_time, comms.id")
1176 first = None
1177 last = None
1178 while query.next():
1179 if first is None:
1180 first = query.value(0)
1181 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1182 last = query.value(0)
1183 if not(last is None):
1184 return last
1185 return first
1186
1187# Container for (x, y) data
1188
1189class XY():
1190 def __init__(self, x=0, y=0):
1191 self.x = x
1192 self.y = y
1193
1194 def __str__(self):
1195 return "XY({}, {})".format(str(self.x), str(self.y))
1196
1197# Container for sub-range data
1198
1199class Subrange():
1200 def __init__(self, lo=0, hi=0):
1201 self.lo = lo
1202 self.hi = hi
1203
1204 def __str__(self):
1205 return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1206
1207# Graph data region base class
1208
1209class GraphDataRegion(object):
1210
1211 def __init__(self, key, title = "", ordinal = ""):
1212 self.key = key
1213 self.title = title
1214 self.ordinal = ordinal
1215
1216# Function to sort GraphDataRegion
1217
1218def GraphDataRegionOrdinal(data_region):
1219 return data_region.ordinal
1220
1221# Attributes for a graph region
1222
1223class GraphRegionAttribute():
1224
1225 def __init__(self, colour):
1226 self.colour = colour
1227
1228# Switch graph data region represents a task
1229
1230class SwitchGraphDataRegion(GraphDataRegion):
1231
1232 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1233 super(SwitchGraphDataRegion, self).__init__(key)
1234
1235 self.title = str(pid) + " / " + str(tid) + " " + comm
1236 # Order graph legend within exec comm by pid / tid / time
1237 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1238 self.exec_comm_id = exec_comm_id
1239 self.pid = pid
1240 self.tid = tid
1241 self.comm = comm
1242 self.thread_id = thread_id
1243 self.comm_id = comm_id
1244
1245# Graph data point
1246
1247class GraphDataPoint():
1248
1249 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1250 self.data = data
1251 self.index = index
1252 self.x = x
1253 self.y = y
1254 self.altx = altx
1255 self.alty = alty
1256 self.hregion = hregion
1257 self.vregion = vregion
1258
1259# Graph data (single graph) base class
1260
1261class GraphData(object):
1262
1263 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1264 self.collection = collection
1265 self.points = []
1266 self.xbase = xbase
1267 self.ybase = ybase
1268 self.title = ""
1269
1270 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1271 index = len(self.points)
1272
1273 x = float(Decimal(x) - self.xbase)
1274 y = float(Decimal(y) - self.ybase)
1275
1276 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1277
1278 def XToData(self, x):
1279 return Decimal(x) + self.xbase
1280
1281 def YToData(self, y):
1282 return Decimal(y) + self.ybase
1283
1284# Switch graph data (for one CPU)
1285
1286class SwitchGraphData(GraphData):
1287
1288 def __init__(self, db, collection, cpu, xbase):
1289 super(SwitchGraphData, self).__init__(collection, xbase)
1290
1291 self.cpu = cpu
1292 self.title = "CPU " + str(cpu)
1293 self.SelectSwitches(db)
1294
1295 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1296 query = QSqlQuery(db)
1297 QueryExec(query, "SELECT id, c_time"
1298 " FROM comms"
1299 " WHERE c_thread_id = " + str(thread_id) +
1300 " AND exec_flag = " + self.collection.glb.dbref.TRUE +
1301 " AND c_time >= " + str(start_time) +
1302 " AND c_time <= " + str(end_time) +
1303 " ORDER BY c_time, id")
1304 while query.next():
1305 comm_id = query.value(0)
1306 if comm_id == last_comm_id:
1307 continue
1308 time = query.value(1)
1309 hregion = self.HRegion(db, thread_id, comm_id, time)
1310 self.AddPoint(time, 1000, None, None, hregion)
1311
1312 def SelectSwitches(self, db):
1313 last_time = None
1314 last_comm_id = None
1315 last_thread_id = None
1316 query = QSqlQuery(db)
1317 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1318 " FROM context_switches"
1319 " WHERE machine_id = " + str(self.collection.machine_id) +
1320 " AND cpu = " + str(self.cpu) +
1321 " ORDER BY time, id")
1322 while query.next():
1323 flags = int(query.value(5))
1324 if flags & 1:
1325 # Schedule-out: detect and add exec's
1326 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1327 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1328 continue
1329 # Schedule-in: add data point
1330 if len(self.points) == 0:
1331 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1332 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1333 self.AddPoint(start_time, 1000, None, None, hregion)
1334 time = query.value(0)
1335 comm_id = query.value(4)
1336 thread_id = query.value(2)
1337 hregion = self.HRegion(db, thread_id, comm_id, time)
1338 self.AddPoint(time, 1000, None, None, hregion)
1339 last_time = time
1340 last_comm_id = comm_id
1341 last_thread_id = thread_id
1342
1343 def NewHRegion(self, db, key, thread_id, comm_id, time):
1344 exec_comm_id = ExecComm(db, thread_id, time)
1345 query = QSqlQuery(db)
1346 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1347 if query.next():
1348 pid = query.value(0)
1349 tid = query.value(1)
1350 else:
1351 pid = -1
1352 tid = -1
1353 query = QSqlQuery(db)
1354 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1355 if query.next():
1356 comm = query.value(0)
1357 else:
1358 comm = ""
1359 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1360
1361 def HRegion(self, db, thread_id, comm_id, time):
1362 key = str(thread_id) + ":" + str(comm_id)
1363 hregion = self.collection.LookupHRegion(key)
1364 if hregion is None:
1365 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1366 self.collection.AddHRegion(key, hregion)
1367 return hregion
1368
1369# Graph data collection (multiple related graphs) base class
1370
1371class GraphDataCollection(object):
1372
1373 def __init__(self, glb):
1374 self.glb = glb
1375 self.data = []
1376 self.hregions = {}
1377 self.xrangelo = None
1378 self.xrangehi = None
1379 self.yrangelo = None
1380 self.yrangehi = None
1381 self.dp = XY(0, 0)
1382
1383 def AddGraphData(self, data):
1384 self.data.append(data)
1385
1386 def LookupHRegion(self, key):
1387 if key in self.hregions:
1388 return self.hregions[key]
1389 return None
1390
1391 def AddHRegion(self, key, hregion):
1392 self.hregions[key] = hregion
1393
1394# Switch graph data collection (SwitchGraphData for each CPU)
1395
1396class SwitchGraphDataCollection(GraphDataCollection):
1397
1398 def __init__(self, glb, db, machine_id):
1399 super(SwitchGraphDataCollection, self).__init__(glb)
1400
1401 self.machine_id = machine_id
1402 self.cpus = self.SelectCPUs(db)
1403
1404 self.xrangelo = glb.StartTime(machine_id)
1405 self.xrangehi = glb.FinishTime(machine_id)
1406
1407 self.yrangelo = Decimal(0)
1408 self.yrangehi = Decimal(1000)
1409
1410 for cpu in self.cpus:
1411 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1412
1413 def SelectCPUs(self, db):
1414 cpus = []
1415 query = QSqlQuery(db)
1416 QueryExec(query, "SELECT DISTINCT cpu"
1417 " FROM context_switches"
1418 " WHERE machine_id = " + str(self.machine_id))
1419 while query.next():
1420 cpus.append(int(query.value(0)))
1421 return sorted(cpus)
1422
1423# Switch graph data graphics item displays the graphed data
1424
1425class SwitchGraphDataGraphicsItem(QGraphicsItem):
1426
1427 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1428 super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1429
1430 self.data = data
1431 self.graph_width = graph_width
1432 self.graph_height = graph_height
1433 self.attrs = attrs
1434 self.event_handler = event_handler
1435 self.setAcceptHoverEvents(True)
1436
1437 def boundingRect(self):
1438 return QRectF(0, 0, self.graph_width, self.graph_height)
1439
1440 def PaintPoint(self, painter, last, x):
1441 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1442 if last.x < self.attrs.subrange.x.lo:
1443 x0 = self.attrs.subrange.x.lo
1444 else:
1445 x0 = last.x
1446 if x > self.attrs.subrange.x.hi:
1447 x1 = self.attrs.subrange.x.hi
1448 else:
1449 x1 = x - 1
1450 x0 = self.attrs.XToPixel(x0)
1451 x1 = self.attrs.XToPixel(x1)
1452
1453 y0 = self.attrs.YToPixel(last.y)
1454
1455 colour = self.attrs.region_attributes[last.hregion.key].colour
1456
1457 width = x1 - x0 + 1
1458 if width < 2:
1459 painter.setPen(colour)
1460 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1461 else:
1462 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1463
1464 def paint(self, painter, option, widget):
1465 last = None
1466 for point in self.data.points:
1467 self.PaintPoint(painter, last, point.x)
1468 if point.x > self.attrs.subrange.x.hi:
1469 break;
1470 last = point
1471 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1472
1473 def BinarySearchPoint(self, target):
1474 lower_pos = 0
1475 higher_pos = len(self.data.points)
1476 while True:
1477 pos = int((lower_pos + higher_pos) / 2)
1478 val = self.data.points[pos].x
1479 if target >= val:
1480 lower_pos = pos
1481 else:
1482 higher_pos = pos
1483 if higher_pos <= lower_pos + 1:
1484 return lower_pos
1485
1486 def XPixelToData(self, x):
1487 x = self.attrs.PixelToX(x)
1488 if x < self.data.points[0].x:
1489 x = 0
1490 pos = 0
1491 low = True
1492 else:
1493 pos = self.BinarySearchPoint(x)
1494 low = False
1495 return (low, pos, self.data.XToData(x))
1496
1497 def EventToData(self, event):
1498 no_data = (None,) * 4
1499 if len(self.data.points) < 1:
1500 return no_data
1501 x = event.pos().x()
1502 if x < 0:
1503 return no_data
1504 low0, pos0, time_from = self.XPixelToData(x)
1505 low1, pos1, time_to = self.XPixelToData(x + 1)
1506 hregions = set()
1507 hregion_times = []
1508 if not low1:
1509 for i in xrange(pos0, pos1 + 1):
1510 hregion = self.data.points[i].hregion
1511 hregions.add(hregion)
1512 if i == pos0:
1513 time = time_from
1514 else:
1515 time = self.data.XToData(self.data.points[i].x)
1516 hregion_times.append((hregion, time))
1517 return (time_from, time_to, hregions, hregion_times)
1518
1519 def hoverMoveEvent(self, event):
1520 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1521 if time_from is not None:
1522 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1523
1524 def hoverLeaveEvent(self, event):
1525 self.event_handler.NoPointEvent()
1526
1527 def mousePressEvent(self, event):
1528 if event.button() != Qt.RightButton:
1529 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1530 return
1531 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1532 if hregion_times:
1533 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1534
1535# X-axis graphics item
1536
1537class XAxisGraphicsItem(QGraphicsItem):
1538
1539 def __init__(self, width, parent=None):
1540 super(XAxisGraphicsItem, self).__init__(parent)
1541
1542 self.width = width
1543 self.max_mark_sz = 4
1544 self.height = self.max_mark_sz + 1
1545
1546 def boundingRect(self):
1547 return QRectF(0, 0, self.width, self.height)
1548
1549 def Step(self):
1550 attrs = self.parentItem().attrs
1551 subrange = attrs.subrange.x
1552 t = subrange.hi - subrange.lo
1553 s = (3.0 * t) / self.width
1554 n = 1.0
1555 while s > n:
1556 n = n * 10.0
1557 return n
1558
1559 def PaintMarks(self, painter, at_y, lo, hi, step, i):
1560 attrs = self.parentItem().attrs
1561 x = lo
1562 while x <= hi:
1563 xp = attrs.XToPixel(x)
1564 if i % 10:
1565 if i % 5:
1566 sz = 1
1567 else:
1568 sz = 2
1569 else:
1570 sz = self.max_mark_sz
1571 i = 0
1572 painter.drawLine(xp, at_y, xp, at_y + sz)
1573 x += step
1574 i += 1
1575
1576 def paint(self, painter, option, widget):
1577 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1578 painter.drawLine(0, 0, self.width - 1, 0)
1579 n = self.Step()
1580 attrs = self.parentItem().attrs
1581 subrange = attrs.subrange.x
1582 if subrange.lo:
1583 x_offset = n - (subrange.lo % n)
1584 else:
1585 x_offset = 0.0
1586 x = subrange.lo + x_offset
1587 i = (x / n) % 10
1588 self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1589
1590 def ScaleDimensions(self):
1591 n = self.Step()
1592 attrs = self.parentItem().attrs
1593 lo = attrs.subrange.x.lo
1594 hi = (n * 10.0) + lo
1595 width = attrs.XToPixel(hi)
1596 if width > 500:
1597 width = 0
1598 return (n, lo, hi, width)
1599
1600 def PaintScale(self, painter, at_x, at_y):
1601 n, lo, hi, width = self.ScaleDimensions()
1602 if not width:
1603 return
1604 painter.drawLine(at_x, at_y, at_x + width, at_y)
1605 self.PaintMarks(painter, at_y, lo, hi, n, 0)
1606
1607 def ScaleWidth(self):
1608 n, lo, hi, width = self.ScaleDimensions()
1609 return width
1610
1611 def ScaleHeight(self):
1612 return self.height
1613
1614 def ScaleUnit(self):
1615 return self.Step() * 10
1616
1617# Scale graphics item base class
1618
1619class ScaleGraphicsItem(QGraphicsItem):
1620
1621 def __init__(self, axis, parent=None):
1622 super(ScaleGraphicsItem, self).__init__(parent)
1623 self.axis = axis
1624
1625 def boundingRect(self):
1626 scale_width = self.axis.ScaleWidth()
1627 if not scale_width:
1628 return QRectF()
1629 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1630
1631 def paint(self, painter, option, widget):
1632 scale_width = self.axis.ScaleWidth()
1633 if not scale_width:
1634 return
1635 self.axis.PaintScale(painter, 0, 5)
1636 x = scale_width + 4
1637 painter.drawText(QPointF(x, 10), self.Text())
1638
1639 def Unit(self):
1640 return self.axis.ScaleUnit()
1641
1642 def Text(self):
1643 return ""
1644
1645# Switch graph scale graphics item
1646
1647class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1648
1649 def __init__(self, axis, parent=None):
1650 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1651
1652 def Text(self):
1653 unit = self.Unit()
1654 if unit >= 1000000000:
1655 unit = int(unit / 1000000000)
1656 us = "s"
1657 elif unit >= 1000000:
1658 unit = int(unit / 1000000)
1659 us = "ms"
1660 elif unit >= 1000:
1661 unit = int(unit / 1000)
1662 us = "us"
1663 else:
1664 unit = int(unit)
1665 us = "ns"
1666 return " = " + str(unit) + " " + us
1667
1668# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1669
1670class SwitchGraphGraphicsItem(QGraphicsItem):
1671
1672 def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1673 super(SwitchGraphGraphicsItem, self).__init__(parent)
1674 self.collection = collection
1675 self.data = data
1676 self.attrs = attrs
1677 self.event_handler = event_handler
1678
1679 margin = 20
1680 title_width = 50
1681
1682 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1683
1684 self.title_graphics.setPos(margin, margin)
1685 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1686 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1687
1688 self.graph_origin_x = margin + title_width + margin
1689 self.graph_origin_y = graph_height + margin
1690
1691 x_axis_size = 1
1692 y_axis_size = 1
1693 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1694
1695 self.x_axis = XAxisGraphicsItem(graph_width, self)
1696 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1697
1698 if first:
1699 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1700 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1701
1702 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1703
1704 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1705 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1706
1707 self.width = self.graph_origin_x + graph_width + margin
1708 self.height = self.graph_origin_y + margin
1709
1710 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1711 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1712
1713 if parent and 'EnableRubberBand' in dir(parent):
1714 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1715
1716 def boundingRect(self):
1717 return QRectF(0, 0, self.width, self.height)
1718
1719 def paint(self, painter, option, widget):
1720 pass
1721
1722 def RBXToPixel(self, x):
1723 return self.attrs.PixelToX(x - self.graph_origin_x)
1724
1725 def RBXRangeToPixel(self, x0, x1):
1726 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1727
1728 def RBPixelToTime(self, x):
1729 if x < self.data.points[0].x:
1730 return self.data.XToData(0)
1731 return self.data.XToData(x)
1732
1733 def RBEventTimes(self, x0, x1):
1734 x0, x1 = self.RBXRangeToPixel(x0, x1)
1735 time_from = self.RBPixelToTime(x0)
1736 time_to = self.RBPixelToTime(x1)
1737 return (time_from, time_to)
1738
1739 def RBEvent(self, x0, x1):
1740 time_from, time_to = self.RBEventTimes(x0, x1)
1741 self.event_handler.RangeEvent(time_from, time_to)
1742
1743 def RBMoveEvent(self, x0, x1):
1744 if x1 < x0:
1745 x0, x1 = x1, x0
1746 self.RBEvent(x0, x1)
1747
1748 def RBReleaseEvent(self, x0, x1, selection_state):
1749 if x1 < x0:
1750 x0, x1 = x1, x0
1751 x0, x1 = self.RBXRangeToPixel(x0, x1)
1752 self.event_handler.SelectEvent(x0, x1, selection_state)
1753
1754# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1755
1756class VerticalBracketGraphicsItem(QGraphicsItem):
1757
1758 def __init__(self, parent=None):
1759 super(VerticalBracketGraphicsItem, self).__init__(parent)
1760
1761 self.width = 0
1762 self.height = 0
1763 self.hide()
1764
1765 def SetSize(self, width, height):
1766 self.width = width + 1
1767 self.height = height + 1
1768
1769 def boundingRect(self):
1770 return QRectF(0, 0, self.width, self.height)
1771
1772 def paint(self, painter, option, widget):
1773 colour = QColor(255, 255, 0, 32)
1774 painter.fillRect(0, 0, self.width, self.height, colour)
1775 x1 = self.width - 1
1776 y1 = self.height - 1
1777 painter.drawLine(0, 0, x1, 0)
1778 painter.drawLine(0, 0, 0, 3)
1779 painter.drawLine(x1, 0, x1, 3)
1780 painter.drawLine(0, y1, x1, y1)
1781 painter.drawLine(0, y1, 0, y1 - 3)
1782 painter.drawLine(x1, y1, x1, y1 - 3)
1783
1784# Graphics item to contain graphs arranged vertically
1785
1786class VertcalGraphSetGraphicsItem(QGraphicsItem):
1787
1788 def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1789 super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1790
1791 self.collection = collection
1792
1793 self.top = 10
1794
1795 self.width = 0
1796 self.height = self.top
1797
1798 self.rubber_band = None
1799 self.rb_enabled = False
1800
1801 first = True
1802 for data in collection.data:
1803 child = child_class(collection, data, attrs, event_handler, first, self)
1804 child.setPos(0, self.height + 1)
1805 rect = child.boundingRect()
1806 if rect.right() > self.width:
1807 self.width = rect.right()
1808 self.height = self.height + rect.bottom() + 1
1809 first = False
1810
1811 self.bracket = VerticalBracketGraphicsItem(self)
1812
1813 def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1814 if self.rb_enabled:
1815 return
1816 self.rb_enabled = True
1817 self.rb_in_view = False
1818 self.setAcceptedMouseButtons(Qt.LeftButton)
1819 self.rb_xlo = xlo
1820 self.rb_xhi = xhi
1821 self.rb_event_handler = rb_event_handler
1822 self.mousePressEvent = self.MousePressEvent
1823 self.mouseMoveEvent = self.MouseMoveEvent
1824 self.mouseReleaseEvent = self.MouseReleaseEvent
1825
1826 def boundingRect(self):
1827 return QRectF(0, 0, self.width, self.height)
1828
1829 def paint(self, painter, option, widget):
1830 pass
1831
1832 def RubberBandParent(self):
1833 scene = self.scene()
1834 view = scene.views()[0]
1835 viewport = view.viewport()
1836 return viewport
1837
1838 def RubberBandSetGeometry(self, rect):
1839 scene_rectf = self.mapRectToScene(QRectF(rect))
1840 scene = self.scene()
1841 view = scene.views()[0]
1842 poly = view.mapFromScene(scene_rectf)
1843 self.rubber_band.setGeometry(poly.boundingRect())
1844
1845 def SetSelection(self, selection_state):
1846 if self.rubber_band:
1847 if selection_state:
1848 self.RubberBandSetGeometry(selection_state)
1849 self.rubber_band.show()
1850 else:
1851 self.rubber_band.hide()
1852
1853 def SetBracket(self, rect):
1854 if rect:
1855 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1856 self.bracket.setPos(x, y)
1857 self.bracket.SetSize(width, height)
1858 self.bracket.show()
1859 else:
1860 self.bracket.hide()
1861
1862 def RubberBandX(self, event):
1863 x = event.pos().toPoint().x()
1864 if x < self.rb_xlo:
1865 x = self.rb_xlo
1866 elif x > self.rb_xhi:
1867 x = self.rb_xhi
1868 else:
1869 self.rb_in_view = True
1870 return x
1871
1872 def RubberBandRect(self, x):
1873 if self.rb_origin.x() <= x:
1874 width = x - self.rb_origin.x()
1875 rect = QRect(self.rb_origin, QSize(width, self.height))
1876 else:
1877 width = self.rb_origin.x() - x
1878 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1879 rect = QRect(top_left, QSize(width, self.height))
1880 return rect
1881
1882 def MousePressEvent(self, event):
1883 self.rb_in_view = False
1884 x = self.RubberBandX(event)
1885 self.rb_origin = QPoint(x, self.top)
1886 if self.rubber_band is None:
1887 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1888 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1889 if self.rb_in_view:
1890 self.rubber_band.show()
1891 self.rb_event_handler.RBMoveEvent(x, x)
1892 else:
1893 self.rubber_band.hide()
1894
1895 def MouseMoveEvent(self, event):
1896 x = self.RubberBandX(event)
1897 rect = self.RubberBandRect(x)
1898 self.RubberBandSetGeometry(rect)
1899 if self.rb_in_view:
1900 self.rubber_band.show()
1901 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1902
1903 def MouseReleaseEvent(self, event):
1904 x = self.RubberBandX(event)
1905 if self.rb_in_view:
1906 selection_state = self.RubberBandRect(x)
1907 else:
1908 selection_state = None
1909 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1910
1911# Switch graph legend data model
1912
1913class SwitchGraphLegendModel(QAbstractTableModel):
1914
1915 def __init__(self, collection, region_attributes, parent=None):
1916 super(SwitchGraphLegendModel, self).__init__(parent)
1917
1918 self.region_attributes = region_attributes
1919
1920 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1921 self.child_count = len(self.child_items)
1922
1923 self.highlight_set = set()
1924
1925 self.column_headers = ("pid", "tid", "comm")
1926
1927 def rowCount(self, parent):
1928 return self.child_count
1929
1930 def headerData(self, section, orientation, role):
1931 if role != Qt.DisplayRole:
1932 return None
1933 if orientation != Qt.Horizontal:
1934 return None
1935 return self.columnHeader(section)
1936
1937 def index(self, row, column, parent):
1938 return self.createIndex(row, column, self.child_items[row])
1939
1940 def columnCount(self, parent=None):
1941 return len(self.column_headers)
1942
1943 def columnHeader(self, column):
1944 return self.column_headers[column]
1945
1946 def data(self, index, role):
1947 if role == Qt.BackgroundRole:
1948 child = self.child_items[index.row()]
1949 if child in self.highlight_set:
1950 return self.region_attributes[child.key].colour
1951 return None
1952 if role == Qt.ForegroundRole:
1953 child = self.child_items[index.row()]
1954 if child in self.highlight_set:
1955 return QColor(255, 255, 255)
1956 return self.region_attributes[child.key].colour
1957 if role != Qt.DisplayRole:
1958 return None
1959 hregion = self.child_items[index.row()]
1960 col = index.column()
1961 if col == 0:
1962 return hregion.pid
1963 if col == 1:
1964 return hregion.tid
1965 if col == 2:
1966 return hregion.comm
1967 return None
1968
1969 def SetHighlight(self, row, set_highlight):
1970 child = self.child_items[row]
1971 top_left = self.createIndex(row, 0, child)
1972 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1973 self.dataChanged.emit(top_left, bottom_right)
1974
1975 def Highlight(self, highlight_set):
1976 for row in xrange(self.child_count):
1977 child = self.child_items[row]
1978 if child in self.highlight_set:
1979 if child not in highlight_set:
1980 self.SetHighlight(row, False)
1981 elif child in highlight_set:
1982 self.SetHighlight(row, True)
1983 self.highlight_set = highlight_set
1984
1985# Switch graph legend is a table
1986
1987class SwitchGraphLegend(QWidget):
1988
1989 def __init__(self, collection, region_attributes, parent=None):
1990 super(SwitchGraphLegend, self).__init__(parent)
1991
1992 self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1993
1994 self.model = QSortFilterProxyModel()
1995 self.model.setSourceModel(self.data_model)
1996
1997 self.view = QTableView()
1998 self.view.setModel(self.model)
1999 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2000 self.view.verticalHeader().setVisible(False)
2001 self.view.sortByColumn(-1, Qt.AscendingOrder)
2002 self.view.setSortingEnabled(True)
2003 self.view.resizeColumnsToContents()
2004 self.view.resizeRowsToContents()
2005
2006 self.vbox = VBoxLayout(self.view)
2007 self.setLayout(self.vbox)
2008
2009 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2010 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2011 self.saved_size = sz1
2012
2013 def resizeEvent(self, event):
2014 self.saved_size = self.size().width()
2015 super(SwitchGraphLegend, self).resizeEvent(event)
2016
2017 def Highlight(self, highlight_set):
2018 self.data_model.Highlight(highlight_set)
2019 self.update()
2020
2021 def changeEvent(self, event):
2022 if event.type() == QEvent.FontChange:
2023 self.view.resizeRowsToContents()
2024 self.view.resizeColumnsToContents()
2025 # Need to resize rows again after column resize
2026 self.view.resizeRowsToContents()
2027 super(SwitchGraphLegend, self).changeEvent(event)
2028
2029# Random colour generation
2030
2031def RGBColourTooLight(r, g, b):
2032 if g > 230:
2033 return True
2034 if g <= 160:
2035 return False
2036 if r <= 180 and g <= 180:
2037 return False
2038 if r < 60:
2039 return False
2040 return True
2041
2042def GenerateColours(x):
2043 cs = [0]
2044 for i in xrange(1, x):
2045 cs.append(int((255.0 / i) + 0.5))
2046 colours = []
2047 for r in cs:
2048 for g in cs:
2049 for b in cs:
2050 # Exclude black and colours that look too light against a white background
2051 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2052 continue
2053 colours.append(QColor(r, g, b))
2054 return colours
2055
2056def GenerateNColours(n):
2057 for x in xrange(2, n + 2):
2058 colours = GenerateColours(x)
2059 if len(colours) >= n:
2060 return colours
2061 return []
2062
2063def GenerateNRandomColours(n, seed):
2064 colours = GenerateNColours(n)
2065 random.seed(seed)
2066 random.shuffle(colours)
2067 return colours
2068
2069# Graph attributes, in particular the scale and subrange that change when zooming
2070
2071class GraphAttributes():
2072
2073 def __init__(self, scale, subrange, region_attributes, dp):
2074 self.scale = scale
2075 self.subrange = subrange
2076 self.region_attributes = region_attributes
2077 # Rounding avoids errors due to finite floating point precision
2078 self.dp = dp # data decimal places
2079 self.Update()
2080
2081 def XToPixel(self, x):
2082 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2083
2084 def YToPixel(self, y):
2085 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2086
2087 def PixelToXRounded(self, px):
2088 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2089
2090 def PixelToYRounded(self, py):
2091 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2092
2093 def PixelToX(self, px):
2094 x = self.PixelToXRounded(px)
2095 if self.pdp.x == 0:
2096 rt = self.XToPixel(x)
2097 if rt > px:
2098 return x - 1
2099 return x
2100
2101 def PixelToY(self, py):
2102 y = self.PixelToYRounded(py)
2103 if self.pdp.y == 0:
2104 rt = self.YToPixel(y)
2105 if rt > py:
2106 return y - 1
2107 return y
2108
2109 def ToPDP(self, dp, scale):
2110 # Calculate pixel decimal places:
2111 # (10 ** dp) is the minimum delta in the data
2112 # scale it to get the minimum delta in pixels
2113 # log10 gives the number of decimals places negatively
2114 # subtrace 1 to divide by 10
2115 # round to the lower negative number
2116 # change the sign to get the number of decimals positively
2117 x = math.log10((10 ** dp) * scale)
2118 if x < 0:
2119 x -= 1
2120 x = -int(math.floor(x) - 0.1)
2121 else:
2122 x = 0
2123 return x
2124
2125 def Update(self):
2126 x = self.ToPDP(self.dp.x, self.scale.x)
2127 y = self.ToPDP(self.dp.y, self.scale.y)
2128 self.pdp = XY(x, y) # pixel decimal places
2129
2130# Switch graph splitter which divides the CPU graphs from the legend
2131
2132class SwitchGraphSplitter(QSplitter):
2133
2134 def __init__(self, parent=None):
2135 super(SwitchGraphSplitter, self).__init__(parent)
2136
2137 self.first_time = False
2138
2139 def resizeEvent(self, ev):
2140 if self.first_time:
2141 self.first_time = False
2142 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2143 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2144 sz0 = self.size().width() - self.handleWidth() - sz1
2145 self.setSizes([sz0, sz1])
2146 elif not(self.widget(1).saved_size is None):
2147 sz1 = self.widget(1).saved_size
2148 sz0 = self.size().width() - self.handleWidth() - sz1
2149 self.setSizes([sz0, sz1])
2150 super(SwitchGraphSplitter, self).resizeEvent(ev)
2151
2152# Graph widget base class
2153
2154class GraphWidget(QWidget):
2155
2156 graph_title_changed = Signal(object)
2157
2158 def __init__(self, parent=None):
2159 super(GraphWidget, self).__init__(parent)
2160
2161 def GraphTitleChanged(self, title):
2162 self.graph_title_changed.emit(title)
2163
2164 def Title(self):
2165 return ""
2166
2167# Display time in s, ms, us or ns
2168
2169def ToTimeStr(val):
2170 val = Decimal(val)
2171 if val >= 1000000000:
2172 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2173 if val >= 1000000:
2174 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2175 if val >= 1000:
2176 return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2177 return "{} ns".format(val.quantize(Decimal("1")))
2178
2179# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2180
2181class SwitchGraphWidget(GraphWidget):
2182
2183 def __init__(self, glb, collection, parent=None):
2184 super(SwitchGraphWidget, self).__init__(parent)
2185
2186 self.glb = glb
2187 self.collection = collection
2188
2189 self.back_state = []
2190 self.forward_state = []
2191 self.selection_state = (None, None)
2192 self.fwd_rect = None
2193 self.start_time = self.glb.StartTime(collection.machine_id)
2194
2195 i = 0
2196 hregions = collection.hregions.values()
2197 colours = GenerateNRandomColours(len(hregions), 1013)
2198 region_attributes = {}
2199 for hregion in hregions:
2200 if hregion.pid == 0 and hregion.tid == 0:
2201 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2202 else:
2203 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2204 i = i + 1
2205
2206 # Default to entire range
2207 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2208 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2209 subrange = XY(xsubrange, ysubrange)
2210
2211 scale = self.GetScaleForRange(subrange)
2212
2213 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2214
2215 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2216
2217 self.scene = QGraphicsScene()
2218 self.scene.addItem(self.item)
2219
2220 self.view = QGraphicsView(self.scene)
2221 self.view.centerOn(0, 0)
2222 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2223
2224 self.legend = SwitchGraphLegend(collection, region_attributes)
2225
2226 self.splitter = SwitchGraphSplitter()
2227 self.splitter.addWidget(self.view)
2228 self.splitter.addWidget(self.legend)
2229
2230 self.point_label = QLabel("")
2231 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2232
2233 self.back_button = QToolButton()
2234 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2235 self.back_button.setDisabled(True)
2236 self.back_button.released.connect(lambda: self.Back())
2237
2238 self.forward_button = QToolButton()
2239 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2240 self.forward_button.setDisabled(True)
2241 self.forward_button.released.connect(lambda: self.Forward())
2242
2243 self.zoom_button = QToolButton()
2244 self.zoom_button.setText("Zoom")
2245 self.zoom_button.setDisabled(True)
2246 self.zoom_button.released.connect(lambda: self.Zoom())
2247
2248 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2249
2250 self.vbox = VBoxLayout(self.splitter, self.hbox)
2251
2252 self.setLayout(self.vbox)
2253
2254 def GetScaleForRangeX(self, xsubrange):
2255 # Default graph 1000 pixels wide
2256 dflt = 1000.0
2257 r = xsubrange.hi - xsubrange.lo
2258 return dflt / r
2259
2260 def GetScaleForRangeY(self, ysubrange):
2261 # Default graph 50 pixels high
2262 dflt = 50.0
2263 r = ysubrange.hi - ysubrange.lo
2264 return dflt / r
2265
2266 def GetScaleForRange(self, subrange):
2267 # Default graph 1000 pixels wide, 50 pixels high
2268 xscale = self.GetScaleForRangeX(subrange.x)
2269 yscale = self.GetScaleForRangeY(subrange.y)
2270 return XY(xscale, yscale)
2271
2272 def PointEvent(self, cpu, time_from, time_to, hregions):
2273 text = "CPU: " + str(cpu)
2274 time_from = time_from.quantize(Decimal(1))
2275 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2276 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2277 self.point_label.setText(text)
2278 self.legend.Highlight(hregions)
2279
2280 def RightClickEvent(self, cpu, hregion_times, pos):
2281 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2282 return
2283 menu = QMenu(self.view)
2284 for hregion, time in hregion_times:
2285 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2286 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2287 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2288 menu.exec_(pos)
2289
2290 def RightClickSelect(self, args):
2291 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2292
2293 def NoPointEvent(self):
2294 self.point_label.setText("")
2295 self.legend.Highlight({})
2296
2297 def RangeEvent(self, time_from, time_to):
2298 time_from = time_from.quantize(Decimal(1))
2299 time_to = time_to.quantize(Decimal(1))
2300 if time_to <= time_from:
2301 self.point_label.setText("")
2302 return
2303 rel_time_from = time_from - self.start_time
2304 rel_time_to = time_to - self.start_time
2305 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2306 text = text + " duration: " + ToTimeStr(time_to - time_from)
2307 self.point_label.setText(text)
2308
2309 def BackState(self):
2310 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2311
2312 def PushBackState(self):
2313 state = copy.deepcopy(self.BackState())
2314 self.back_state.append(state)
2315 self.back_button.setEnabled(True)
2316
2317 def PopBackState(self):
2318 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2319 self.attrs.Update()
2320 if not self.back_state:
2321 self.back_button.setDisabled(True)
2322
2323 def PushForwardState(self):
2324 state = copy.deepcopy(self.BackState())
2325 self.forward_state.append(state)
2326 self.forward_button.setEnabled(True)
2327
2328 def PopForwardState(self):
2329 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2330 self.attrs.Update()
2331 if not self.forward_state:
2332 self.forward_button.setDisabled(True)
2333
2334 def Title(self):
2335 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2336 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2337 rel_time_from = time_from - self.start_time
2338 rel_time_to = time_to - self.start_time
2339 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2340 title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2341 return title
2342
2343 def Update(self):
2344 selected_subrange, selection_state = self.selection_state
2345 self.item.SetSelection(selection_state)
2346 self.item.SetBracket(self.fwd_rect)
2347 self.zoom_button.setDisabled(selected_subrange is None)
2348 self.GraphTitleChanged(self.Title())
2349 self.item.update(self.item.boundingRect())
2350
2351 def Back(self):
2352 if not self.back_state:
2353 return
2354 self.PushForwardState()
2355 self.PopBackState()
2356 self.Update()
2357
2358 def Forward(self):
2359 if not self.forward_state:
2360 return
2361 self.PushBackState()
2362 self.PopForwardState()
2363 self.Update()
2364
2365 def SelectEvent(self, x0, x1, selection_state):
2366 if selection_state is None:
2367 selected_subrange = None
2368 else:
2369 if x1 - x0 < 1.0:
2370 x1 += 1.0
2371 selected_subrange = Subrange(x0, x1)
2372 self.selection_state = (selected_subrange, selection_state)
2373 self.zoom_button.setDisabled(selected_subrange is None)
2374
2375 def Zoom(self):
2376 selected_subrange, selection_state = self.selection_state
2377 if selected_subrange is None:
2378 return
2379 self.fwd_rect = selection_state
2380 self.item.SetSelection(None)
2381 self.PushBackState()
2382 self.attrs.subrange.x = selected_subrange
2383 self.forward_state = []
2384 self.forward_button.setDisabled(True)
2385 self.selection_state = (None, None)
2386 self.fwd_rect = None
2387 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2388 self.attrs.Update()
2389 self.Update()
2390
2391# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2392
2393class SlowInitClass():
2394
2395 def __init__(self, glb, title, init_fn):
2396 self.init_fn = init_fn
2397 self.done = False
2398 self.result = None
2399
2400 self.msg_box = QMessageBox(glb.mainwindow)
2401 self.msg_box.setText("Initializing " + title + ". Please wait.")
2402 self.msg_box.setWindowTitle("Initializing " + title)
2403 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2404
2405 self.init_thread = Thread(self.ThreadFn, glb)
2406 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2407
2408 self.init_thread.start()
2409
2410 def Done(self):
2411 self.msg_box.done(0)
2412
2413 def ThreadFn(self, glb):
2414 conn_name = "SlowInitClass" + str(os.getpid())
2415 db, dbname = glb.dbref.Open(conn_name)
2416 self.result = self.init_fn(db)
2417 self.done = True
2418 return (True, 0)
2419
2420 def Result(self):
2421 while not self.done:
2422 self.msg_box.exec_()
2423 self.init_thread.wait()
2424 return self.result
2425
2426def SlowInit(glb, title, init_fn):
2427 init = SlowInitClass(glb, title, init_fn)
2428 return init.Result()
2429
2430# Time chart by CPU window
2431
2432class TimeChartByCPUWindow(QMdiSubWindow):
2433
2434 def __init__(self, glb, parent=None):
2435 super(TimeChartByCPUWindow, self).__init__(parent)
2436
2437 self.glb = glb
2438 self.machine_id = glb.HostMachineId()
2439 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2440
2441 collection = LookupModel(self.collection_name)
2442 if collection is None:
2443 collection = SlowInit(glb, "Time Chart", self.Init)
2444
2445 self.widget = SwitchGraphWidget(glb, collection, self)
2446 self.view = self.widget
2447
2448 self.base_title = "Time Chart by CPU"
2449 self.setWindowTitle(self.base_title + self.widget.Title())
2450 self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2451
2452 self.setWidget(self.widget)
2453
2454 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2455
2456 def Init(self, db):
2457 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2458
2459 def GraphTitleChanged(self, title):
2460 self.setWindowTitle(self.base_title + " : " + title)
2461
2462# Child data item finder
2463
2464class ChildDataItemFinder():
2465
2466 def __init__(self, root):
2467 self.root = root
2468 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2469 self.rows = []
2470 self.pos = 0
2471
2472 def FindSelect(self):
2473 self.rows = []
2474 if self.pattern:
2475 pattern = re.compile(self.value)
2476 for child in self.root.child_items:
2477 for column_data in child.data:
2478 if re.search(pattern, str(column_data)) is not None:
2479 self.rows.append(child.row)
2480 break
2481 else:
2482 for child in self.root.child_items:
2483 for column_data in child.data:
2484 if self.value in str(column_data):
2485 self.rows.append(child.row)
2486 break
2487
2488 def FindValue(self):
2489 self.pos = 0
2490 if self.last_value != self.value or self.pattern != self.last_pattern:
2491 self.FindSelect()
2492 if not len(self.rows):
2493 return -1
2494 return self.rows[self.pos]
2495
2496 def FindThread(self):
2497 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2498 row = self.FindValue()
2499 elif len(self.rows):
2500 if self.direction > 0:
2501 self.pos += 1
2502 if self.pos >= len(self.rows):
2503 self.pos = 0
2504 else:
2505 self.pos -= 1
2506 if self.pos < 0:
2507 self.pos = len(self.rows) - 1
2508 row = self.rows[self.pos]
2509 else:
2510 row = -1
2511 return (True, row)
2512
2513 def Find(self, value, direction, pattern, context, callback):
2514 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2515 # Use a thread so the UI is not blocked
2516 thread = Thread(self.FindThread)
2517 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2518 thread.start()
2519
2520 def FindDone(self, thread, callback, row):
2521 callback(row)
2522
2523# Number of database records to fetch in one go
2524
2525glb_chunk_sz = 10000
2526
2527# Background process for SQL data fetcher
2528
2529class SQLFetcherProcess():
2530
2531 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2532 # Need a unique connection name
2533 conn_name = "SQLFetcher" + str(os.getpid())
2534 self.db, dbname = dbref.Open(conn_name)
2535 self.sql = sql
2536 self.buffer = buffer
2537 self.head = head
2538 self.tail = tail
2539 self.fetch_count = fetch_count
2540 self.fetching_done = fetching_done
2541 self.process_target = process_target
2542 self.wait_event = wait_event
2543 self.fetched_event = fetched_event
2544 self.prep = prep
2545 self.query = QSqlQuery(self.db)
2546 self.query_limit = 0 if "$$last_id$$" in sql else 2
2547 self.last_id = -1
2548 self.fetched = 0
2549 self.more = True
2550 self.local_head = self.head.value
2551 self.local_tail = self.tail.value
2552
2553 def Select(self):
2554 if self.query_limit:
2555 if self.query_limit == 1:
2556 return
2557 self.query_limit -= 1
2558 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2559 QueryExec(self.query, stmt)
2560
2561 def Next(self):
2562 if not self.query.next():
2563 self.Select()
2564 if not self.query.next():
2565 return None
2566 self.last_id = self.query.value(0)
2567 return self.prep(self.query)
2568
2569 def WaitForTarget(self):
2570 while True:
2571 self.wait_event.clear()
2572 target = self.process_target.value
2573 if target > self.fetched or target < 0:
2574 break
2575 self.wait_event.wait()
2576 return target
2577
2578 def HasSpace(self, sz):
2579 if self.local_tail <= self.local_head:
2580 space = len(self.buffer) - self.local_head
2581 if space > sz:
2582 return True
2583 if space >= glb_nsz:
2584 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2585 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2586 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2587 self.local_head = 0
2588 if self.local_tail - self.local_head > sz:
2589 return True
2590 return False
2591
2592 def WaitForSpace(self, sz):
2593 if self.HasSpace(sz):
2594 return
2595 while True:
2596 self.wait_event.clear()
2597 self.local_tail = self.tail.value
2598 if self.HasSpace(sz):
2599 return
2600 self.wait_event.wait()
2601
2602 def AddToBuffer(self, obj):
2603 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2604 n = len(d)
2605 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2606 sz = n + glb_nsz
2607 self.WaitForSpace(sz)
2608 pos = self.local_head
2609 self.buffer[pos : pos + len(nd)] = nd
2610 self.buffer[pos + glb_nsz : pos + sz] = d
2611 self.local_head += sz
2612
2613 def FetchBatch(self, batch_size):
2614 fetched = 0
2615 while batch_size > fetched:
2616 obj = self.Next()
2617 if obj is None:
2618 self.more = False
2619 break
2620 self.AddToBuffer(obj)
2621 fetched += 1
2622 if fetched:
2623 self.fetched += fetched
2624 with self.fetch_count.get_lock():
2625 self.fetch_count.value += fetched
2626 self.head.value = self.local_head
2627 self.fetched_event.set()
2628
2629 def Run(self):
2630 while self.more:
2631 target = self.WaitForTarget()
2632 if target < 0:
2633 break
2634 batch_size = min(glb_chunk_sz, target - self.fetched)
2635 self.FetchBatch(batch_size)
2636 self.fetching_done.value = True
2637 self.fetched_event.set()
2638
2639def SQLFetcherFn(*x):
2640 process = SQLFetcherProcess(*x)
2641 process.Run()
2642
2643# SQL data fetcher
2644
2645class SQLFetcher(QObject):
2646
2647 done = Signal(object)
2648
2649 def __init__(self, glb, sql, prep, process_data, parent=None):
2650 super(SQLFetcher, self).__init__(parent)
2651 self.process_data = process_data
2652 self.more = True
2653 self.target = 0
2654 self.last_target = 0
2655 self.fetched = 0
2656 self.buffer_size = 16 * 1024 * 1024
2657 self.buffer = Array(c_char, self.buffer_size, lock=False)
2658 self.head = Value(c_longlong)
2659 self.tail = Value(c_longlong)
2660 self.local_tail = 0
2661 self.fetch_count = Value(c_longlong)
2662 self.fetching_done = Value(c_bool)
2663 self.last_count = 0
2664 self.process_target = Value(c_longlong)
2665 self.wait_event = Event()
2666 self.fetched_event = Event()
2667 glb.AddInstanceToShutdownOnExit(self)
2668 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))
2669 self.process.start()
2670 self.thread = Thread(self.Thread)
2671 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2672 self.thread.start()
2673
2674 def Shutdown(self):
2675 # Tell the thread and process to exit
2676 self.process_target.value = -1
2677 self.wait_event.set()
2678 self.more = False
2679 self.fetching_done.value = True
2680 self.fetched_event.set()
2681
2682 def Thread(self):
2683 if not self.more:
2684 return True, 0
2685 while True:
2686 self.fetched_event.clear()
2687 fetch_count = self.fetch_count.value
2688 if fetch_count != self.last_count:
2689 break
2690 if self.fetching_done.value:
2691 self.more = False
2692 return True, 0
2693 self.fetched_event.wait()
2694 count = fetch_count - self.last_count
2695 self.last_count = fetch_count
2696 self.fetched += count
2697 return False, count
2698
2699 def Fetch(self, nr):
2700 if not self.more:
2701 # -1 inidcates there are no more
2702 return -1
2703 result = self.fetched
2704 extra = result + nr - self.target
2705 if extra > 0:
2706 self.target += extra
2707 # process_target < 0 indicates shutting down
2708 if self.process_target.value >= 0:
2709 self.process_target.value = self.target
2710 self.wait_event.set()
2711 return result
2712
2713 def RemoveFromBuffer(self):
2714 pos = self.local_tail
2715 if len(self.buffer) - pos < glb_nsz:
2716 pos = 0
2717 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2718 if n == 0:
2719 pos = 0
2720 n = pickle.loads(self.buffer[0 : glb_nsz])
2721 pos += glb_nsz
2722 obj = pickle.loads(self.buffer[pos : pos + n])
2723 self.local_tail = pos + n
2724 return obj
2725
2726 def ProcessData(self, count):
2727 for i in xrange(count):
2728 obj = self.RemoveFromBuffer()
2729 self.process_data(obj)
2730 self.tail.value = self.local_tail
2731 self.wait_event.set()
2732 self.done.emit(count)
2733
2734# Fetch more records bar
2735
2736class FetchMoreRecordsBar():
2737
2738 def __init__(self, model, parent):
2739 self.model = model
2740
2741 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2742 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2743
2744 self.fetch_count = QSpinBox()
2745 self.fetch_count.setRange(1, 1000000)
2746 self.fetch_count.setValue(10)
2747 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2748
2749 self.fetch = QPushButton("Go!")
2750 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2751 self.fetch.released.connect(self.FetchMoreRecords)
2752
2753 self.progress = QProgressBar()
2754 self.progress.setRange(0, 100)
2755 self.progress.hide()
2756
2757 self.done_label = QLabel("All records fetched")
2758 self.done_label.hide()
2759
2760 self.spacer = QLabel("")
2761
2762 self.close_button = QToolButton()
2763 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2764 self.close_button.released.connect(self.Deactivate)
2765
2766 self.hbox = QHBoxLayout()
2767 self.hbox.setContentsMargins(0, 0, 0, 0)
2768
2769 self.hbox.addWidget(self.label)
2770 self.hbox.addWidget(self.fetch_count)
2771 self.hbox.addWidget(self.fetch)
2772 self.hbox.addWidget(self.spacer)
2773 self.hbox.addWidget(self.progress)
2774 self.hbox.addWidget(self.done_label)
2775 self.hbox.addWidget(self.close_button)
2776
2777 self.bar = QWidget()
2778 self.bar.setLayout(self.hbox)
2779 self.bar.show()
2780
2781 self.in_progress = False
2782 self.model.progress.connect(self.Progress)
2783
2784 self.done = False
2785
2786 if not model.HasMoreRecords():
2787 self.Done()
2788
2789 def Widget(self):
2790 return self.bar
2791
2792 def Activate(self):
2793 self.bar.show()
2794 self.fetch.setFocus()
2795
2796 def Deactivate(self):
2797 self.bar.hide()
2798
2799 def Enable(self, enable):
2800 self.fetch.setEnabled(enable)
2801 self.fetch_count.setEnabled(enable)
2802
2803 def Busy(self):
2804 self.Enable(False)
2805 self.fetch.hide()
2806 self.spacer.hide()
2807 self.progress.show()
2808
2809 def Idle(self):
2810 self.in_progress = False
2811 self.Enable(True)
2812 self.progress.hide()
2813 self.fetch.show()
2814 self.spacer.show()
2815
2816 def Target(self):
2817 return self.fetch_count.value() * glb_chunk_sz
2818
2819 def Done(self):
2820 self.done = True
2821 self.Idle()
2822 self.label.hide()
2823 self.fetch_count.hide()
2824 self.fetch.hide()
2825 self.spacer.hide()
2826 self.done_label.show()
2827
2828 def Progress(self, count):
2829 if self.in_progress:
2830 if count:
2831 percent = ((count - self.start) * 100) / self.Target()
2832 if percent >= 100:
2833 self.Idle()
2834 else:
2835 self.progress.setValue(percent)
2836 if not count:
2837 # Count value of zero means no more records
2838 self.Done()
2839
2840 def FetchMoreRecords(self):
2841 if self.done:
2842 return
2843 self.progress.setValue(0)
2844 self.Busy()
2845 self.in_progress = True
2846 self.start = self.model.FetchMoreRecords(self.Target())
2847
2848# Brance data model level two item
2849
2850class BranchLevelTwoItem():
2851
2852 def __init__(self, row, col, text, parent_item):
2853 self.row = row
2854 self.parent_item = parent_item
2855 self.data = [""] * (col + 1)
2856 self.data[col] = text
2857 self.level = 2
2858
2859 def getParentItem(self):
2860 return self.parent_item
2861
2862 def getRow(self):
2863 return self.row
2864
2865 def childCount(self):
2866 return 0
2867
2868 def hasChildren(self):
2869 return False
2870
2871 def getData(self, column):
2872 return self.data[column]
2873
2874# Brance data model level one item
2875
2876class BranchLevelOneItem():
2877
2878 def __init__(self, glb, row, data, parent_item):
2879 self.glb = glb
2880 self.row = row
2881 self.parent_item = parent_item
2882 self.child_count = 0
2883 self.child_items = []
2884 self.data = data[1:]
2885 self.dbid = data[0]
2886 self.level = 1
2887 self.query_done = False
2888 self.br_col = len(self.data) - 1
2889
2890 def getChildItem(self, row):
2891 return self.child_items[row]
2892
2893 def getParentItem(self):
2894 return self.parent_item
2895
2896 def getRow(self):
2897 return self.row
2898
2899 def Select(self):
2900 self.query_done = True
2901
2902 if not self.glb.have_disassembler:
2903 return
2904
2905 query = QSqlQuery(self.glb.db)
2906
2907 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2908 " FROM samples"
2909 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2910 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2911 " WHERE samples.id = " + str(self.dbid))
2912 if not query.next():
2913 return
2914 cpu = query.value(0)
2915 dso = query.value(1)
2916 sym = query.value(2)
2917 if dso == 0 or sym == 0:
2918 return
2919 off = query.value(3)
2920 short_name = query.value(4)
2921 long_name = query.value(5)
2922 build_id = query.value(6)
2923 sym_start = query.value(7)
2924 ip = query.value(8)
2925
2926 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2927 " FROM samples"
2928 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2929 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2930 " ORDER BY samples.id"
2931 " LIMIT 1")
2932 if not query.next():
2933 return
2934 if query.value(0) != dso:
2935 # Cannot disassemble from one dso to another
2936 return
2937 bsym = query.value(1)
2938 boff = query.value(2)
2939 bsym_start = query.value(3)
2940 if bsym == 0:
2941 return
2942 tot = bsym_start + boff + 1 - sym_start - off
2943 if tot <= 0 or tot > 16384:
2944 return
2945
2946 inst = self.glb.disassembler.Instruction()
2947 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2948 if not f:
2949 return
2950 mode = 0 if Is64Bit(f) else 1
2951 self.glb.disassembler.SetMode(inst, mode)
2952
2953 buf_sz = tot + 16
2954 buf = create_string_buffer(tot + 16)
2955 f.seek(sym_start + off)
2956 buf.value = f.read(buf_sz)
2957 buf_ptr = addressof(buf)
2958 i = 0
2959 while tot > 0:
2960 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2961 if cnt:
2962 byte_str = tohex(ip).rjust(16)
2963 for k in xrange(cnt):
2964 byte_str += " %02x" % ord(buf[i])
2965 i += 1
2966 while k < 15:
2967 byte_str += " "
2968 k += 1
2969 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2970 self.child_count += 1
2971 else:
2972 return
2973 buf_ptr += cnt
2974 tot -= cnt
2975 buf_sz -= cnt
2976 ip += cnt
2977
2978 def childCount(self):
2979 if not self.query_done:
2980 self.Select()
2981 if not self.child_count:
2982 return -1
2983 return self.child_count
2984
2985 def hasChildren(self):
2986 if not self.query_done:
2987 return True
2988 return self.child_count > 0
2989
2990 def getData(self, column):
2991 return self.data[column]
2992
2993# Brance data model root item
2994
2995class BranchRootItem():
2996
2997 def __init__(self):
2998 self.child_count = 0
2999 self.child_items = []
3000 self.level = 0
3001
3002 def getChildItem(self, row):
3003 return self.child_items[row]
3004
3005 def getParentItem(self):
3006 return None
3007
3008 def getRow(self):
3009 return 0
3010
3011 def childCount(self):
3012 return self.child_count
3013
3014 def hasChildren(self):
3015 return self.child_count > 0
3016
3017 def getData(self, column):
3018 return ""
3019
3020# Calculate instructions per cycle
3021
3022def CalcIPC(cyc_cnt, insn_cnt):
3023 if cyc_cnt and insn_cnt:
3024 ipc = Decimal(float(insn_cnt) / cyc_cnt)
3025 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3026 else:
3027 ipc = "0"
3028 return ipc
3029
3030# Branch data preparation
3031
3032def BranchDataPrepBr(query, data):
3033 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3034 " (" + dsoname(query.value(11)) + ")" + " -> " +
3035 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3036 " (" + dsoname(query.value(15)) + ")")
3037
3038def BranchDataPrepIPC(query, data):
3039 insn_cnt = query.value(16)
3040 cyc_cnt = query.value(17)
3041 ipc = CalcIPC(cyc_cnt, insn_cnt)
3042 data.append(insn_cnt)
3043 data.append(cyc_cnt)
3044 data.append(ipc)
3045
3046def BranchDataPrep(query):
3047 data = []
3048 for i in xrange(0, 8):
3049 data.append(query.value(i))
3050 BranchDataPrepBr(query, data)
3051 return data
3052
3053def BranchDataPrepWA(query):
3054 data = []
3055 data.append(query.value(0))
3056 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3057 data.append("{:>19}".format(query.value(1)))
3058 for i in xrange(2, 8):
3059 data.append(query.value(i))
3060 BranchDataPrepBr(query, data)
3061 return data
3062
3063def BranchDataWithIPCPrep(query):
3064 data = []
3065 for i in xrange(0, 8):
3066 data.append(query.value(i))
3067 BranchDataPrepIPC(query, data)
3068 BranchDataPrepBr(query, data)
3069 return data
3070
3071def BranchDataWithIPCPrepWA(query):
3072 data = []
3073 data.append(query.value(0))
3074 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3075 data.append("{:>19}".format(query.value(1)))
3076 for i in xrange(2, 8):
3077 data.append(query.value(i))
3078 BranchDataPrepIPC(query, data)
3079 BranchDataPrepBr(query, data)
3080 return data
3081
3082# Branch data model
3083
3084class BranchModel(TreeModel):
3085
3086 progress = Signal(object)
3087
3088 def __init__(self, glb, event_id, where_clause, parent=None):
3089 super(BranchModel, self).__init__(glb, None, parent)
3090 self.event_id = event_id
3091 self.more = True
3092 self.populated = 0
3093 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3094 if self.have_ipc:
3095 select_ipc = ", insn_count, cyc_count"
3096 prep_fn = BranchDataWithIPCPrep
3097 prep_wa_fn = BranchDataWithIPCPrepWA
3098 else:
3099 select_ipc = ""
3100 prep_fn = BranchDataPrep
3101 prep_wa_fn = BranchDataPrepWA
3102 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3103 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3104 " ip, symbols.name, sym_offset, dsos.short_name,"
3105 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3106 + select_ipc +
3107 " FROM samples"
3108 " INNER JOIN comms ON comm_id = comms.id"
3109 " INNER JOIN threads ON thread_id = threads.id"
3110 " INNER JOIN branch_types ON branch_type = branch_types.id"
3111 " INNER JOIN symbols ON symbol_id = symbols.id"
3112 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3113 " INNER JOIN dsos ON samples.dso_id = dsos.id"
3114 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3115 " WHERE samples.id > $$last_id$$" + where_clause +
3116 " AND evsel_id = " + str(self.event_id) +
3117 " ORDER BY samples.id"
3118 " LIMIT " + str(glb_chunk_sz))
3119 if pyside_version_1 and sys.version_info[0] == 3:
3120 prep = prep_fn
3121 else:
3122 prep = prep_wa_fn
3123 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3124 self.fetcher.done.connect(self.Update)
3125 self.fetcher.Fetch(glb_chunk_sz)
3126
3127 def GetRoot(self):
3128 return BranchRootItem()
3129
3130 def columnCount(self, parent=None):
3131 if self.have_ipc:
3132 return 11
3133 else:
3134 return 8
3135
3136 def columnHeader(self, column):
3137 if self.have_ipc:
3138 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3139 else:
3140 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3141
3142 def columnFont(self, column):
3143 if self.have_ipc:
3144 br_col = 10
3145 else:
3146 br_col = 7
3147 if column != br_col:
3148 return None
3149 return QFont("Monospace")
3150
3151 def DisplayData(self, item, index):
3152 if item.level == 1:
3153 self.FetchIfNeeded(item.row)
3154 return item.getData(index.column())
3155
3156 def AddSample(self, data):
3157 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3158 self.root.child_items.append(child)
3159 self.populated += 1
3160
3161 def Update(self, fetched):
3162 if not fetched:
3163 self.more = False
3164 self.progress.emit(0)
3165 child_count = self.root.child_count
3166 count = self.populated - child_count
3167 if count > 0:
3168 parent = QModelIndex()
3169 self.beginInsertRows(parent, child_count, child_count + count - 1)
3170 self.insertRows(child_count, count, parent)
3171 self.root.child_count += count
3172 self.endInsertRows()
3173 self.progress.emit(self.root.child_count)
3174
3175 def FetchMoreRecords(self, count):
3176 current = self.root.child_count
3177 if self.more:
3178 self.fetcher.Fetch(count)
3179 else:
3180 self.progress.emit(0)
3181 return current
3182
3183 def HasMoreRecords(self):
3184 return self.more
3185
3186# Report Variables
3187
3188class ReportVars():
3189
3190 def __init__(self, name = "", where_clause = "", limit = ""):
3191 self.name = name
3192 self.where_clause = where_clause
3193 self.limit = limit
3194
3195 def UniqueId(self):
3196 return str(self.where_clause + ";" + self.limit)
3197
3198# Branch window
3199
3200class BranchWindow(QMdiSubWindow):
3201
3202 def __init__(self, glb, event_id, report_vars, parent=None):
3203 super(BranchWindow, self).__init__(parent)
3204
3205 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
3206
3207 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3208
3209 self.view = QTreeView()
3210 self.view.setUniformRowHeights(True)
3211 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3212 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3213 self.view.setModel(self.model)
3214
3215 self.ResizeColumnsToContents()
3216
3217 self.context_menu = TreeContextMenu(self.view)
3218
3219 self.find_bar = FindBar(self, self, True)
3220
3221 self.finder = ChildDataItemFinder(self.model.root)
3222
3223 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3224
3225 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3226
3227 self.setWidget(self.vbox.Widget())
3228
3229 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3230
3231 def ResizeColumnToContents(self, column, n):
3232 # Using the view's resizeColumnToContents() here is extrememly slow
3233 # so implement a crude alternative
3234 mm = "MM" if column else "MMMM"
3235 font = self.view.font()
3236 metrics = QFontMetrics(font)
3237 max = 0
3238 for row in xrange(n):
3239 val = self.model.root.child_items[row].data[column]
3240 len = metrics.width(str(val) + mm)
3241 max = len if len > max else max
3242 val = self.model.columnHeader(column)
3243 len = metrics.width(str(val) + mm)
3244 max = len if len > max else max
3245 self.view.setColumnWidth(column, max)
3246
3247 def ResizeColumnsToContents(self):
3248 n = min(self.model.root.child_count, 100)
3249 if n < 1:
3250 # No data yet, so connect a signal to notify when there is
3251 self.model.rowsInserted.connect(self.UpdateColumnWidths)
3252 return
3253 columns = self.model.columnCount()
3254 for i in xrange(columns):
3255 self.ResizeColumnToContents(i, n)
3256
3257 def UpdateColumnWidths(self, *x):
3258 # This only needs to be done once, so disconnect the signal now
3259 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3260 self.ResizeColumnsToContents()
3261
3262 def Find(self, value, direction, pattern, context):
3263 self.view.setFocus()
3264 self.find_bar.Busy()
3265 self.finder.Find(value, direction, pattern, context, self.FindDone)
3266
3267 def FindDone(self, row):
3268 self.find_bar.Idle()
3269 if row >= 0:
3270 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3271 else:
3272 self.find_bar.NotFound()
3273
3274# Line edit data item
3275
3276class LineEditDataItem(object):
3277
3278 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3279 self.glb = glb
3280 self.label = label
3281 self.placeholder_text = placeholder_text
3282 self.parent = parent
3283 self.id = id
3284
3285 self.value = default
3286
3287 self.widget = QLineEdit(default)
3288 self.widget.editingFinished.connect(self.Validate)
3289 self.widget.textChanged.connect(self.Invalidate)
3290 self.red = False
3291 self.error = ""
3292 self.validated = True
3293
3294 if placeholder_text:
3295 self.widget.setPlaceholderText(placeholder_text)
3296
3297 def TurnTextRed(self):
3298 if not self.red:
3299 palette = QPalette()
3300 palette.setColor(QPalette.Text,Qt.red)
3301 self.widget.setPalette(palette)
3302 self.red = True
3303
3304 def TurnTextNormal(self):
3305 if self.red:
3306 palette = QPalette()
3307 self.widget.setPalette(palette)
3308 self.red = False
3309
3310 def InvalidValue(self, value):
3311 self.value = ""
3312 self.TurnTextRed()
3313 self.error = self.label + " invalid value '" + value + "'"
3314 self.parent.ShowMessage(self.error)
3315
3316 def Invalidate(self):
3317 self.validated = False
3318
3319 def DoValidate(self, input_string):
3320 self.value = input_string.strip()
3321
3322 def Validate(self):
3323 self.validated = True
3324 self.error = ""
3325 self.TurnTextNormal()
3326 self.parent.ClearMessage()
3327 input_string = self.widget.text()
3328 if not len(input_string.strip()):
3329 self.value = ""
3330 return
3331 self.DoValidate(input_string)
3332
3333 def IsValid(self):
3334 if not self.validated:
3335 self.Validate()
3336 if len(self.error):
3337 self.parent.ShowMessage(self.error)
3338 return False
3339 return True
3340
3341 def IsNumber(self, value):
3342 try:
3343 x = int(value)
3344 except:
3345 x = 0
3346 return str(x) == value
3347
3348# Non-negative integer ranges dialog data item
3349
3350class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3351
3352 def __init__(self, glb, label, placeholder_text, column_name, parent):
3353 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3354
3355 self.column_name = column_name
3356
3357 def DoValidate(self, input_string):
3358 singles = []
3359 ranges = []
3360 for value in [x.strip() for x in input_string.split(",")]:
3361 if "-" in value:
3362 vrange = value.split("-")
3363 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3364 return self.InvalidValue(value)
3365 ranges.append(vrange)
3366 else:
3367 if not self.IsNumber(value):
3368 return self.InvalidValue(value)
3369 singles.append(value)
3370 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3371 if len(singles):
3372 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3373 self.value = " OR ".join(ranges)
3374
3375# Positive integer dialog data item
3376
3377class PositiveIntegerDataItem(LineEditDataItem):
3378
3379 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3380 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3381
3382 def DoValidate(self, input_string):
3383 if not self.IsNumber(input_string.strip()):
3384 return self.InvalidValue(input_string)
3385 value = int(input_string.strip())
3386 if value <= 0:
3387 return self.InvalidValue(input_string)
3388 self.value = str(value)
3389
3390# Dialog data item converted and validated using a SQL table
3391
3392class SQLTableDataItem(LineEditDataItem):
3393
3394 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3395 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3396
3397 self.table_name = table_name
3398 self.match_column = match_column
3399 self.column_name1 = column_name1
3400 self.column_name2 = column_name2
3401
3402 def ValueToIds(self, value):
3403 ids = []
3404 query = QSqlQuery(self.glb.db)
3405 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3406 ret = query.exec_(stmt)
3407 if ret:
3408 while query.next():
3409 ids.append(str(query.value(0)))
3410 return ids
3411
3412 def DoValidate(self, input_string):
3413 all_ids = []
3414 for value in [x.strip() for x in input_string.split(",")]:
3415 ids = self.ValueToIds(value)
3416 if len(ids):
3417 all_ids.extend(ids)
3418 else:
3419 return self.InvalidValue(value)
3420 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3421 if self.column_name2:
3422 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3423
3424# Sample time ranges dialog data item converted and validated using 'samples' SQL table
3425
3426class SampleTimeRangesDataItem(LineEditDataItem):
3427
3428 def __init__(self, glb, label, placeholder_text, column_name, parent):
3429 self.column_name = column_name
3430
3431 self.last_id = 0
3432 self.first_time = 0
3433 self.last_time = 2 ** 64
3434
3435 query = QSqlQuery(glb.db)
3436 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3437 if query.next():
3438 self.last_id = int(query.value(0))
3439 self.first_time = int(glb.HostStartTime())
3440 self.last_time = int(glb.HostFinishTime())
3441 if placeholder_text:
3442 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3443
3444 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3445
3446 def IdBetween(self, query, lower_id, higher_id, order):
3447 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3448 if query.next():
3449 return True, int(query.value(0))
3450 else:
3451 return False, 0
3452
3453 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3454 query = QSqlQuery(self.glb.db)
3455 while True:
3456 next_id = int((lower_id + higher_id) / 2)
3457 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3458 if not query.next():
3459 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3460 if not ok:
3461 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3462 if not ok:
3463 return str(higher_id)
3464 next_id = dbid
3465 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3466 next_time = int(query.value(0))
3467 if get_floor:
3468 if target_time > next_time:
3469 lower_id = next_id
3470 else:
3471 higher_id = next_id
3472 if higher_id <= lower_id + 1:
3473 return str(higher_id)
3474 else:
3475 if target_time >= next_time:
3476 lower_id = next_id
3477 else:
3478 higher_id = next_id
3479 if higher_id <= lower_id + 1:
3480 return str(lower_id)
3481
3482 def ConvertRelativeTime(self, val):
3483 mult = 1
3484 suffix = val[-2:]
3485 if suffix == "ms":
3486 mult = 1000000
3487 elif suffix == "us":
3488 mult = 1000
3489 elif suffix == "ns":
3490 mult = 1
3491 else:
3492 return val
3493 val = val[:-2].strip()
3494 if not self.IsNumber(val):
3495 return val
3496 val = int(val) * mult
3497 if val >= 0:
3498 val += self.first_time
3499 else:
3500 val += self.last_time
3501 return str(val)
3502
3503 def ConvertTimeRange(self, vrange):
3504 if vrange[0] == "":
3505 vrange[0] = str(self.first_time)
3506 if vrange[1] == "":
3507 vrange[1] = str(self.last_time)
3508 vrange[0] = self.ConvertRelativeTime(vrange[0])
3509 vrange[1] = self.ConvertRelativeTime(vrange[1])
3510 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3511 return False
3512 beg_range = max(int(vrange[0]), self.first_time)
3513 end_range = min(int(vrange[1]), self.last_time)
3514 if beg_range > self.last_time or end_range < self.first_time:
3515 return False
3516 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3517 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3518 return True
3519
3520 def AddTimeRange(self, value, ranges):
3521 n = value.count("-")
3522 if n == 1:
3523 pass
3524 elif n == 2:
3525 if value.split("-")[1].strip() == "":
3526 n = 1
3527 elif n == 3:
3528 n = 2
3529 else:
3530 return False
3531 pos = findnth(value, "-", n)
3532 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3533 if self.ConvertTimeRange(vrange):
3534 ranges.append(vrange)
3535 return True
3536 return False
3537
3538 def DoValidate(self, input_string):
3539 ranges = []
3540 for value in [x.strip() for x in input_string.split(",")]:
3541 if not self.AddTimeRange(value, ranges):
3542 return self.InvalidValue(value)
3543 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3544 self.value = " OR ".join(ranges)
3545
3546# Report Dialog Base
3547
3548class ReportDialogBase(QDialog):
3549
3550 def __init__(self, glb, title, items, partial, parent=None):
3551 super(ReportDialogBase, self).__init__(parent)
3552
3553 self.glb = glb
3554
3555 self.report_vars = ReportVars()
3556
3557 self.setWindowTitle(title)
3558 self.setMinimumWidth(600)
3559
3560 self.data_items = [x(glb, self) for x in items]
3561
3562 self.partial = partial
3563
3564 self.grid = QGridLayout()
3565
3566 for row in xrange(len(self.data_items)):
3567 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3568 self.grid.addWidget(self.data_items[row].widget, row, 1)
3569
3570 self.status = QLabel()
3571
3572 self.ok_button = QPushButton("Ok", self)
3573 self.ok_button.setDefault(True)
3574 self.ok_button.released.connect(self.Ok)
3575 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3576
3577 self.cancel_button = QPushButton("Cancel", self)
3578 self.cancel_button.released.connect(self.reject)
3579 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3580
3581 self.hbox = QHBoxLayout()
3582 #self.hbox.addStretch()
3583 self.hbox.addWidget(self.status)
3584 self.hbox.addWidget(self.ok_button)
3585 self.hbox.addWidget(self.cancel_button)
3586
3587 self.vbox = QVBoxLayout()
3588 self.vbox.addLayout(self.grid)
3589 self.vbox.addLayout(self.hbox)
3590
3591 self.setLayout(self.vbox)
3592
3593 def Ok(self):
3594 vars = self.report_vars
3595 for d in self.data_items:
3596 if d.id == "REPORTNAME":
3597 vars.name = d.value
3598 if not vars.name:
3599 self.ShowMessage("Report name is required")
3600 return
3601 for d in self.data_items:
3602 if not d.IsValid():
3603 return
3604 for d in self.data_items[1:]:
3605 if d.id == "LIMIT":
3606 vars.limit = d.value
3607 elif len(d.value):
3608 if len(vars.where_clause):
3609 vars.where_clause += " AND "
3610 vars.where_clause += d.value
3611 if len(vars.where_clause):
3612 if self.partial:
3613 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3614 else:
3615 vars.where_clause = " WHERE " + vars.where_clause + " "
3616 self.accept()
3617
3618 def ShowMessage(self, msg):
3619 self.status.setText("<font color=#FF0000>" + msg)
3620
3621 def ClearMessage(self):
3622 self.status.setText("")
3623
3624# Selected branch report creation dialog
3625
3626class SelectedBranchDialog(ReportDialogBase):
3627
3628 def __init__(self, glb, parent=None):
3629 title = "Selected Branches"
3630 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3631 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3632 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3633 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3634 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3635 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3636 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3637 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3638 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3639 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3640
3641# Event list
3642
3643def GetEventList(db):
3644 events = []
3645 query = QSqlQuery(db)
3646 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3647 while query.next():
3648 events.append(query.value(0))
3649 return events
3650
3651# Is a table selectable
3652
3653def IsSelectable(db, table, sql = "", columns = "*"):
3654 query = QSqlQuery(db)
3655 try:
3656 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3657 except:
3658 return False
3659 return True
3660
3661# SQL table data model item
3662
3663class SQLTableItem():
3664
3665 def __init__(self, row, data):
3666 self.row = row
3667 self.data = data
3668
3669 def getData(self, column):
3670 return self.data[column]
3671
3672# SQL table data model
3673
3674class SQLTableModel(TableModel):
3675
3676 progress = Signal(object)
3677
3678 def __init__(self, glb, sql, column_headers, parent=None):
3679 super(SQLTableModel, self).__init__(parent)
3680 self.glb = glb
3681 self.more = True
3682 self.populated = 0
3683 self.column_headers = column_headers
3684 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3685 self.fetcher.done.connect(self.Update)
3686 self.fetcher.Fetch(glb_chunk_sz)
3687
3688 def DisplayData(self, item, index):
3689 self.FetchIfNeeded(item.row)
3690 return item.getData(index.column())
3691
3692 def AddSample(self, data):
3693 child = SQLTableItem(self.populated, data)
3694 self.child_items.append(child)
3695 self.populated += 1
3696
3697 def Update(self, fetched):
3698 if not fetched:
3699 self.more = False
3700 self.progress.emit(0)
3701 child_count = self.child_count
3702 count = self.populated - child_count
3703 if count > 0:
3704 parent = QModelIndex()
3705 self.beginInsertRows(parent, child_count, child_count + count - 1)
3706 self.insertRows(child_count, count, parent)
3707 self.child_count += count
3708 self.endInsertRows()
3709 self.progress.emit(self.child_count)
3710
3711 def FetchMoreRecords(self, count):
3712 current = self.child_count
3713 if self.more:
3714 self.fetcher.Fetch(count)
3715 else:
3716 self.progress.emit(0)
3717 return current
3718
3719 def HasMoreRecords(self):
3720 return self.more
3721
3722 def columnCount(self, parent=None):
3723 return len(self.column_headers)
3724
3725 def columnHeader(self, column):
3726 return self.column_headers[column]
3727
3728 def SQLTableDataPrep(self, query, count):
3729 data = []
3730 for i in xrange(count):
3731 data.append(query.value(i))
3732 return data
3733
3734# SQL automatic table data model
3735
3736class SQLAutoTableModel(SQLTableModel):
3737
3738 def __init__(self, glb, table_name, parent=None):
3739 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3740 if table_name == "comm_threads_view":
3741 # For now, comm_threads_view has no id column
3742 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3743 column_headers = []
3744 query = QSqlQuery(glb.db)
3745 if glb.dbref.is_sqlite3:
3746 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3747 while query.next():
3748 column_headers.append(query.value(1))
3749 if table_name == "sqlite_master":
3750 sql = "SELECT * FROM " + table_name
3751 else:
3752 if table_name[:19] == "information_schema.":
3753 sql = "SELECT * FROM " + table_name
3754 select_table_name = table_name[19:]
3755 schema = "information_schema"
3756 else:
3757 select_table_name = table_name
3758 schema = "public"
3759 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3760 while query.next():
3761 column_headers.append(query.value(0))
3762 if pyside_version_1 and sys.version_info[0] == 3:
3763 if table_name == "samples_view":
3764 self.SQLTableDataPrep = self.samples_view_DataPrep
3765 if table_name == "samples":
3766 self.SQLTableDataPrep = self.samples_DataPrep
3767 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3768
3769 def samples_view_DataPrep(self, query, count):
3770 data = []
3771 data.append(query.value(0))
3772 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3773 data.append("{:>19}".format(query.value(1)))
3774 for i in xrange(2, count):
3775 data.append(query.value(i))
3776 return data
3777
3778 def samples_DataPrep(self, query, count):
3779 data = []
3780 for i in xrange(9):
3781 data.append(query.value(i))
3782 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3783 data.append("{:>19}".format(query.value(9)))
3784 for i in xrange(10, count):
3785 data.append(query.value(i))
3786 return data
3787
3788# Base class for custom ResizeColumnsToContents
3789
3790class ResizeColumnsToContentsBase(QObject):
3791
3792 def __init__(self, parent=None):
3793 super(ResizeColumnsToContentsBase, self).__init__(parent)
3794
3795 def ResizeColumnToContents(self, column, n):
3796 # Using the view's resizeColumnToContents() here is extrememly slow
3797 # so implement a crude alternative
3798 font = self.view.font()
3799 metrics = QFontMetrics(font)
3800 max = 0
3801 for row in xrange(n):
3802 val = self.data_model.child_items[row].data[column]
3803 len = metrics.width(str(val) + "MM")
3804 max = len if len > max else max
3805 val = self.data_model.columnHeader(column)
3806 len = metrics.width(str(val) + "MM")
3807 max = len if len > max else max
3808 self.view.setColumnWidth(column, max)
3809
3810 def ResizeColumnsToContents(self):
3811 n = min(self.data_model.child_count, 100)
3812 if n < 1:
3813 # No data yet, so connect a signal to notify when there is
3814 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3815 return
3816 columns = self.data_model.columnCount()
3817 for i in xrange(columns):
3818 self.ResizeColumnToContents(i, n)
3819
3820 def UpdateColumnWidths(self, *x):
3821 # This only needs to be done once, so disconnect the signal now
3822 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3823 self.ResizeColumnsToContents()
3824
3825# Convert value to CSV
3826
3827def ToCSValue(val):
3828 if '"' in val:
3829 val = val.replace('"', '""')
3830 if "," in val or '"' in val:
3831 val = '"' + val + '"'
3832 return val
3833
3834# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3835
3836glb_max_cols = 1000
3837
3838def RowColumnKey(a):
3839 return a.row() * glb_max_cols + a.column()
3840
3841# Copy selected table cells to clipboard
3842
3843def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3844 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3845 idx_cnt = len(indexes)
3846 if not idx_cnt:
3847 return
3848 if idx_cnt == 1:
3849 with_hdr=False
3850 min_row = indexes[0].row()
3851 max_row = indexes[0].row()
3852 min_col = indexes[0].column()
3853 max_col = indexes[0].column()
3854 for i in indexes:
3855 min_row = min(min_row, i.row())
3856 max_row = max(max_row, i.row())
3857 min_col = min(min_col, i.column())
3858 max_col = max(max_col, i.column())
3859 if max_col > glb_max_cols:
3860 raise RuntimeError("glb_max_cols is too low")
3861 max_width = [0] * (1 + max_col - min_col)
3862 for i in indexes:
3863 c = i.column() - min_col
3864 max_width[c] = max(max_width[c], len(str(i.data())))
3865 text = ""
3866 pad = ""
3867 sep = ""
3868 if with_hdr:
3869 model = indexes[0].model()
3870 for col in range(min_col, max_col + 1):
3871 val = model.headerData(col, Qt.Horizontal)
3872 if as_csv:
3873 text += sep + ToCSValue(val)
3874 sep = ","
3875 else:
3876 c = col - min_col
3877 max_width[c] = max(max_width[c], len(val))
3878 width = max_width[c]
3879 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3880 if align & Qt.AlignRight:
3881 val = val.rjust(width)
3882 text += pad + sep + val
3883 pad = " " * (width - len(val))
3884 sep = " "
3885 text += "\n"
3886 pad = ""
3887 sep = ""
3888 last_row = min_row
3889 for i in indexes:
3890 if i.row() > last_row:
3891 last_row = i.row()
3892 text += "\n"
3893 pad = ""
3894 sep = ""
3895 if as_csv:
3896 text += sep + ToCSValue(str(i.data()))
3897 sep = ","
3898 else:
3899 width = max_width[i.column() - min_col]
3900 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3901 val = str(i.data()).rjust(width)
3902 else:
3903 val = str(i.data())
3904 text += pad + sep + val
3905 pad = " " * (width - len(val))
3906 sep = " "
3907 QApplication.clipboard().setText(text)
3908
3909def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3910 indexes = view.selectedIndexes()
3911 if not len(indexes):
3912 return
3913
3914 selection = view.selectionModel()
3915
3916 first = None
3917 for i in indexes:
3918 above = view.indexAbove(i)
3919 if not selection.isSelected(above):
3920 first = i
3921 break
3922
3923 if first is None:
3924 raise RuntimeError("CopyTreeCellsToClipboard internal error")
3925
3926 model = first.model()
3927 row_cnt = 0
3928 col_cnt = model.columnCount(first)
3929 max_width = [0] * col_cnt
3930
3931 indent_sz = 2
3932 indent_str = " " * indent_sz
3933
3934 expanded_mark_sz = 2
3935 if sys.version_info[0] == 3:
3936 expanded_mark = "\u25BC "
3937 not_expanded_mark = "\u25B6 "
3938 else:
3939 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3940 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3941 leaf_mark = " "
3942
3943 if not as_csv:
3944 pos = first
3945 while True:
3946 row_cnt += 1
3947 row = pos.row()
3948 for c in range(col_cnt):
3949 i = pos.sibling(row, c)
3950 if c:
3951 n = len(str(i.data()))
3952 else:
3953 n = len(str(i.data()).strip())
3954 n += (i.internalPointer().level - 1) * indent_sz
3955 n += expanded_mark_sz
3956 max_width[c] = max(max_width[c], n)
3957 pos = view.indexBelow(pos)
3958 if not selection.isSelected(pos):
3959 break
3960
3961 text = ""
3962 pad = ""
3963 sep = ""
3964 if with_hdr:
3965 for c in range(col_cnt):
3966 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3967 if as_csv:
3968 text += sep + ToCSValue(val)
3969 sep = ","
3970 else:
3971 max_width[c] = max(max_width[c], len(val))
3972 width = max_width[c]
3973 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3974 if align & Qt.AlignRight:
3975 val = val.rjust(width)
3976 text += pad + sep + val
3977 pad = " " * (width - len(val))
3978 sep = " "
3979 text += "\n"
3980 pad = ""
3981 sep = ""
3982
3983 pos = first
3984 while True:
3985 row = pos.row()
3986 for c in range(col_cnt):
3987 i = pos.sibling(row, c)
3988 val = str(i.data())
3989 if not c:
3990 if model.hasChildren(i):
3991 if view.isExpanded(i):
3992 mark = expanded_mark
3993 else:
3994 mark = not_expanded_mark
3995 else:
3996 mark = leaf_mark
3997 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
3998 if as_csv:
3999 text += sep + ToCSValue(val)
4000 sep = ","
4001 else:
4002 width = max_width[c]
4003 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
4004 val = val.rjust(width)
4005 text += pad + sep + val
4006 pad = " " * (width - len(val))
4007 sep = " "
4008 pos = view.indexBelow(pos)
4009 if not selection.isSelected(pos):
4010 break
4011 text = text.rstrip() + "\n"
4012 pad = ""
4013 sep = ""
4014
4015 QApplication.clipboard().setText(text)
4016
4017def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4018 view.CopyCellsToClipboard(view, as_csv, with_hdr)
4019
4020def CopyCellsToClipboardHdr(view):
4021 CopyCellsToClipboard(view, False, True)
4022
4023def CopyCellsToClipboardCSV(view):
4024 CopyCellsToClipboard(view, True, True)
4025
4026# Context menu
4027
4028class ContextMenu(object):
4029
4030 def __init__(self, view):
4031 self.view = view
4032 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4033 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4034
4035 def ShowContextMenu(self, pos):
4036 menu = QMenu(self.view)
4037 self.AddActions(menu)
4038 menu.exec_(self.view.mapToGlobal(pos))
4039
4040 def AddCopy(self, menu):
4041 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4042 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4043
4044 def AddActions(self, menu):
4045 self.AddCopy(menu)
4046
4047class TreeContextMenu(ContextMenu):
4048
4049 def __init__(self, view):
4050 super(TreeContextMenu, self).__init__(view)
4051
4052 def AddActions(self, menu):
4053 i = self.view.currentIndex()
4054 text = str(i.data()).strip()
4055 if len(text):
4056 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4057 self.AddCopy(menu)
4058
4059# Table window
4060
4061class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4062
4063 def __init__(self, glb, table_name, parent=None):
4064 super(TableWindow, self).__init__(parent)
4065
4066 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4067
4068 self.model = QSortFilterProxyModel()
4069 self.model.setSourceModel(self.data_model)
4070
4071 self.view = QTableView()
4072 self.view.setModel(self.model)
4073 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4074 self.view.verticalHeader().setVisible(False)
4075 self.view.sortByColumn(-1, Qt.AscendingOrder)
4076 self.view.setSortingEnabled(True)
4077 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4078 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4079
4080 self.ResizeColumnsToContents()
4081
4082 self.context_menu = ContextMenu(self.view)
4083
4084 self.find_bar = FindBar(self, self, True)
4085
4086 self.finder = ChildDataItemFinder(self.data_model)
4087
4088 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4089
4090 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4091
4092 self.setWidget(self.vbox.Widget())
4093
4094 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4095
4096 def Find(self, value, direction, pattern, context):
4097 self.view.setFocus()
4098 self.find_bar.Busy()
4099 self.finder.Find(value, direction, pattern, context, self.FindDone)
4100
4101 def FindDone(self, row):
4102 self.find_bar.Idle()
4103 if row >= 0:
4104 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4105 else:
4106 self.find_bar.NotFound()
4107
4108# Table list
4109
4110def GetTableList(glb):
4111 tables = []
4112 query = QSqlQuery(glb.db)
4113 if glb.dbref.is_sqlite3:
4114 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4115 else:
4116 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4117 while query.next():
4118 tables.append(query.value(0))
4119 if glb.dbref.is_sqlite3:
4120 tables.append("sqlite_master")
4121 else:
4122 tables.append("information_schema.tables")
4123 tables.append("information_schema.views")
4124 tables.append("information_schema.columns")
4125 return tables
4126
4127# Top Calls data model
4128
4129class TopCallsModel(SQLTableModel):
4130
4131 def __init__(self, glb, report_vars, parent=None):
4132 text = ""
4133 if not glb.dbref.is_sqlite3:
4134 text = "::text"
4135 limit = ""
4136 if len(report_vars.limit):
4137 limit = " LIMIT " + report_vars.limit
4138 sql = ("SELECT comm, pid, tid, name,"
4139 " CASE"
4140 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4141 " ELSE short_name"
4142 " END AS dso,"
4143 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4144 " CASE"
4145 " WHEN (calls.flags = 1) THEN 'no call'" + text +
4146 " WHEN (calls.flags = 2) THEN 'no return'" + text +
4147 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4148 " ELSE ''" + text +
4149 " END AS flags"
4150 " FROM calls"
4151 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4152 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4153 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4154 " INNER JOIN comms ON calls.comm_id = comms.id"
4155 " INNER JOIN threads ON calls.thread_id = threads.id" +
4156 report_vars.where_clause +
4157 " ORDER BY elapsed_time DESC" +
4158 limit
4159 )
4160 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4161 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4162 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4163
4164 def columnAlignment(self, column):
4165 return self.alignment[column]
4166
4167# Top Calls report creation dialog
4168
4169class TopCallsDialog(ReportDialogBase):
4170
4171 def __init__(self, glb, parent=None):
4172 title = "Top Calls by Elapsed Time"
4173 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4174 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4175 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4176 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4177 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4178 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4179 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4180 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4181 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4182
4183# Top Calls window
4184
4185class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4186
4187 def __init__(self, glb, report_vars, parent=None):
4188 super(TopCallsWindow, self).__init__(parent)
4189
4190 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4191 self.model = self.data_model
4192
4193 self.view = QTableView()
4194 self.view.setModel(self.model)
4195 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4196 self.view.verticalHeader().setVisible(False)
4197 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4198 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4199
4200 self.context_menu = ContextMenu(self.view)
4201
4202 self.ResizeColumnsToContents()
4203
4204 self.find_bar = FindBar(self, self, True)
4205
4206 self.finder = ChildDataItemFinder(self.model)
4207
4208 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4209
4210 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4211
4212 self.setWidget(self.vbox.Widget())
4213
4214 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4215
4216 def Find(self, value, direction, pattern, context):
4217 self.view.setFocus()
4218 self.find_bar.Busy()
4219 self.finder.Find(value, direction, pattern, context, self.FindDone)
4220
4221 def FindDone(self, row):
4222 self.find_bar.Idle()
4223 if row >= 0:
4224 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4225 else:
4226 self.find_bar.NotFound()
4227
4228# Action Definition
4229
4230def CreateAction(label, tip, callback, parent=None, shortcut=None):
4231 action = QAction(label, parent)
4232 if shortcut != None:
4233 action.setShortcuts(shortcut)
4234 action.setStatusTip(tip)
4235 action.triggered.connect(callback)
4236 return action
4237
4238# Typical application actions
4239
4240def CreateExitAction(app, parent=None):
4241 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4242
4243# Typical MDI actions
4244
4245def CreateCloseActiveWindowAction(mdi_area):
4246 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4247
4248def CreateCloseAllWindowsAction(mdi_area):
4249 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4250
4251def CreateTileWindowsAction(mdi_area):
4252 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4253
4254def CreateCascadeWindowsAction(mdi_area):
4255 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4256
4257def CreateNextWindowAction(mdi_area):
4258 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4259
4260def CreatePreviousWindowAction(mdi_area):
4261 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4262
4263# Typical MDI window menu
4264
4265class WindowMenu():
4266
4267 def __init__(self, mdi_area, menu):
4268 self.mdi_area = mdi_area
4269 self.window_menu = menu.addMenu("&Windows")
4270 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4271 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4272 self.tile_windows = CreateTileWindowsAction(mdi_area)
4273 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4274 self.next_window = CreateNextWindowAction(mdi_area)
4275 self.previous_window = CreatePreviousWindowAction(mdi_area)
4276 self.window_menu.aboutToShow.connect(self.Update)
4277
4278 def Update(self):
4279 self.window_menu.clear()
4280 sub_window_count = len(self.mdi_area.subWindowList())
4281 have_sub_windows = sub_window_count != 0
4282 self.close_active_window.setEnabled(have_sub_windows)
4283 self.close_all_windows.setEnabled(have_sub_windows)
4284 self.tile_windows.setEnabled(have_sub_windows)
4285 self.cascade_windows.setEnabled(have_sub_windows)
4286 self.next_window.setEnabled(have_sub_windows)
4287 self.previous_window.setEnabled(have_sub_windows)
4288 self.window_menu.addAction(self.close_active_window)
4289 self.window_menu.addAction(self.close_all_windows)
4290 self.window_menu.addSeparator()
4291 self.window_menu.addAction(self.tile_windows)
4292 self.window_menu.addAction(self.cascade_windows)
4293 self.window_menu.addSeparator()
4294 self.window_menu.addAction(self.next_window)
4295 self.window_menu.addAction(self.previous_window)
4296 if sub_window_count == 0:
4297 return
4298 self.window_menu.addSeparator()
4299 nr = 1
4300 for sub_window in self.mdi_area.subWindowList():
4301 label = str(nr) + " " + sub_window.name
4302 if nr < 10:
4303 label = "&" + label
4304 action = self.window_menu.addAction(label)
4305 action.setCheckable(True)
4306 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4307 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4308 self.window_menu.addAction(action)
4309 nr += 1
4310
4311 def setActiveSubWindow(self, nr):
4312 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4313
4314# Help text
4315
4316glb_help_text = """
4317<h1>Contents</h1>
4318<style>
4319p.c1 {
4320 text-indent: 40px;
4321}
4322p.c2 {
4323 text-indent: 80px;
4324}
4325}
4326</style>
4327<p class=c1><a href=#reports>1. Reports</a></p>
4328<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4329<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4330<p class=c2><a href=#allbranches>1.3 All branches</a></p>
4331<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4332<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4333<p class=c1><a href=#charts>2. Charts</a></p>
4334<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4335<p class=c1><a href=#tables>3. Tables</a></p>
4336<h1 id=reports>1. Reports</h1>
4337<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4338The result is a GUI window with a tree representing a context-sensitive
4339call-graph. Expanding a couple of levels of the tree and adjusting column
4340widths to suit will display something like:
4341<pre>
4342 Call Graph: pt_example
4343Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
4344v- ls
4345 v- 2638:2638
4346 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
4347 |- unknown unknown 1 13198 0.1 1 0.0
4348 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
4349 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
4350 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
4351 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
4352 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
4353 >- __libc_csu_init ls 1 10354 0.1 10 0.0
4354 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
4355 v- main ls 1 8182043 99.6 180254 99.9
4356</pre>
4357<h3>Points to note:</h3>
4358<ul>
4359<li>The top level is a command name (comm)</li>
4360<li>The next level is a thread (pid:tid)</li>
4361<li>Subsequent levels are functions</li>
4362<li>'Count' is the number of calls</li>
4363<li>'Time' is the elapsed time until the function returns</li>
4364<li>Percentages are relative to the level above</li>
4365<li>'Branch Count' is the total number of branches for that function and all functions that it calls
4366</ul>
4367<h3>Find</h3>
4368Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4369The pattern matching symbols are ? for any character and * for zero or more characters.
4370<h2 id=calltree>1.2 Call Tree</h2>
4371The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4372Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4373<h2 id=allbranches>1.3 All branches</h2>
4374The All branches report displays all branches in chronological order.
4375Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4376<h3>Disassembly</h3>
4377Open a branch to display disassembly. This only works if:
4378<ol>
4379<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4380<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4381The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4382One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4383or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4384</ol>
4385<h4 id=xed>Intel XED Setup</h4>
4386To use Intel XED, libxed.so must be present. To build and install libxed.so:
4387<pre>
4388git clone https://github.com/intelxed/mbuild.git mbuild
4389git clone https://github.com/intelxed/xed
4390cd xed
4391./mfile.py --share
4392sudo ./mfile.py --prefix=/usr/local install
4393sudo ldconfig
4394</pre>
4395<h3>Instructions per Cycle (IPC)</h3>
4396If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4397<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4398Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4399In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4400since the previous displayed 'IPC'.
4401<h3>Find</h3>
4402Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4403Refer to Python documentation for the regular expression syntax.
4404All columns are searched, but only currently fetched rows are searched.
4405<h2 id=selectedbranches>1.4 Selected branches</h2>
4406This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4407by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4408<h3>1.4.1 Time ranges</h3>
4409The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4410ms, us or ns. Also, negative values are relative to the end of trace. Examples:
4411<pre>
4412 81073085947329-81073085958238 From 81073085947329 to 81073085958238
4413 100us-200us From 100us to 200us
4414 10ms- From 10ms to the end
4415 -100ns The first 100ns
4416 -10ms- The last 10ms
4417</pre>
4418N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4419<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4420The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
4421The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4422If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4423<h1 id=charts>2. Charts</h1>
4424<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4425This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4426<h3>Features</h3>
4427<ol>
4428<li>Mouse over to highight the task and show the time</li>
4429<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4430<li>Go back and forward by pressing the arrow buttons</li>
4431<li>If call information is available, right-click to show a call tree opened to that task and time.
4432Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4433</li>
4434</ol>
4435<h3>Important</h3>
4436The graph can be misleading in the following respects:
4437<ol>
4438<li>The graph shows the first task on each CPU as running from the beginning of the time range.
4439Because tracing might start on different CPUs at different times, that is not necessarily the case.
4440Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4441<li>Similarly, the last task on each CPU can be showing running longer than it really was.
4442Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4443<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>
4444</ol>
4445<h1 id=tables>3. Tables</h1>
4446The Tables menu shows all tables and views in the database. Most tables have an associated view
4447which displays the information in a more friendly way. Not all data for large tables is fetched
4448immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4449but that can be slow for large tables.
4450<p>There are also tables of database meta-information.
4451For SQLite3 databases, the sqlite_master table is included.
4452For PostgreSQL databases, information_schema.tables/views/columns are included.
4453<h3>Find</h3>
4454Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4455Refer to Python documentation for the regular expression syntax.
4456All columns are searched, but only currently fetched rows are searched.
4457<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4458will go to the next/previous result in id order, instead of display order.
4459"""
4460
4461# Help window
4462
4463class HelpWindow(QMdiSubWindow):
4464
4465 def __init__(self, glb, parent=None):
4466 super(HelpWindow, self).__init__(parent)
4467
4468 self.text = QTextBrowser()
4469 self.text.setHtml(glb_help_text)
4470 self.text.setReadOnly(True)
4471 self.text.setOpenExternalLinks(True)
4472
4473 self.setWidget(self.text)
4474
4475 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4476
4477# Main window that only displays the help text
4478
4479class HelpOnlyWindow(QMainWindow):
4480
4481 def __init__(self, parent=None):
4482 super(HelpOnlyWindow, self).__init__(parent)
4483
4484 self.setMinimumSize(200, 100)
4485 self.resize(800, 600)
4486 self.setWindowTitle("Exported SQL Viewer Help")
4487 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4488
4489 self.text = QTextBrowser()
4490 self.text.setHtml(glb_help_text)
4491 self.text.setReadOnly(True)
4492 self.text.setOpenExternalLinks(True)
4493
4494 self.setCentralWidget(self.text)
4495
4496# PostqreSQL server version
4497
4498def PostqreSQLServerVersion(db):
4499 query = QSqlQuery(db)
4500 QueryExec(query, "SELECT VERSION()")
4501 if query.next():
4502 v_str = query.value(0)
4503 v_list = v_str.strip().split(" ")
4504 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4505 return v_list[1]
4506 return v_str
4507 return "Unknown"
4508
4509# SQLite version
4510
4511def SQLiteVersion(db):
4512 query = QSqlQuery(db)
4513 QueryExec(query, "SELECT sqlite_version()")
4514 if query.next():
4515 return query.value(0)
4516 return "Unknown"
4517
4518# About dialog
4519
4520class AboutDialog(QDialog):
4521
4522 def __init__(self, glb, parent=None):
4523 super(AboutDialog, self).__init__(parent)
4524
4525 self.setWindowTitle("About Exported SQL Viewer")
4526 self.setMinimumWidth(300)
4527
4528 pyside_version = "1" if pyside_version_1 else "2"
4529
4530 text = "<pre>"
4531 text += "Python version: " + sys.version.split(" ")[0] + "\n"
4532 text += "PySide version: " + pyside_version + "\n"
4533 text += "Qt version: " + qVersion() + "\n"
4534 if glb.dbref.is_sqlite3:
4535 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
4536 else:
4537 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4538 text += "</pre>"
4539
4540 self.text = QTextBrowser()
4541 self.text.setHtml(text)
4542 self.text.setReadOnly(True)
4543 self.text.setOpenExternalLinks(True)
4544
4545 self.vbox = QVBoxLayout()
4546 self.vbox.addWidget(self.text)
4547
4548 self.setLayout(self.vbox)
4549
4550# Font resize
4551
4552def ResizeFont(widget, diff):
4553 font = widget.font()
4554 sz = font.pointSize()
4555 font.setPointSize(sz + diff)
4556 widget.setFont(font)
4557
4558def ShrinkFont(widget):
4559 ResizeFont(widget, -1)
4560
4561def EnlargeFont(widget):
4562 ResizeFont(widget, 1)
4563
4564# Unique name for sub-windows
4565
4566def NumberedWindowName(name, nr):
4567 if nr > 1:
4568 name += " <" + str(nr) + ">"
4569 return name
4570
4571def UniqueSubWindowName(mdi_area, name):
4572 nr = 1
4573 while True:
4574 unique_name = NumberedWindowName(name, nr)
4575 ok = True
4576 for sub_window in mdi_area.subWindowList():
4577 if sub_window.name == unique_name:
4578 ok = False
4579 break
4580 if ok:
4581 return unique_name
4582 nr += 1
4583
4584# Add a sub-window
4585
4586def AddSubWindow(mdi_area, sub_window, name):
4587 unique_name = UniqueSubWindowName(mdi_area, name)
4588 sub_window.setMinimumSize(200, 100)
4589 sub_window.resize(800, 600)
4590 sub_window.setWindowTitle(unique_name)
4591 sub_window.setAttribute(Qt.WA_DeleteOnClose)
4592 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4593 sub_window.name = unique_name
4594 mdi_area.addSubWindow(sub_window)
4595 sub_window.show()
4596
4597# Main window
4598
4599class MainWindow(QMainWindow):
4600
4601 def __init__(self, glb, parent=None):
4602 super(MainWindow, self).__init__(parent)
4603
4604 self.glb = glb
4605
4606 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4607 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4608 self.setMinimumSize(200, 100)
4609
4610 self.mdi_area = QMdiArea()
4611 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4612 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4613
4614 self.setCentralWidget(self.mdi_area)
4615
4616 menu = self.menuBar()
4617
4618 file_menu = menu.addMenu("&File")
4619 file_menu.addAction(CreateExitAction(glb.app, self))
4620
4621 edit_menu = menu.addMenu("&Edit")
4622 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4623 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4624 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4625 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4626 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4627 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4628
4629 reports_menu = menu.addMenu("&Reports")
4630 if IsSelectable(glb.db, "calls"):
4631 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4632
4633 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4634 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4635
4636 self.EventMenu(GetEventList(glb.db), reports_menu)
4637
4638 if IsSelectable(glb.db, "calls"):
4639 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4640
4641 if IsSelectable(glb.db, "context_switches"):
4642 charts_menu = menu.addMenu("&Charts")
4643 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4644
4645 self.TableMenu(GetTableList(glb), menu)
4646
4647 self.window_menu = WindowMenu(self.mdi_area, menu)
4648
4649 help_menu = menu.addMenu("&Help")
4650 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4651 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4652
4653 def Try(self, fn):
4654 win = self.mdi_area.activeSubWindow()
4655 if win:
4656 try:
4657 fn(win.view)
4658 except:
4659 pass
4660
4661 def CopyToClipboard(self):
4662 self.Try(CopyCellsToClipboardHdr)
4663
4664 def CopyToClipboardCSV(self):
4665 self.Try(CopyCellsToClipboardCSV)
4666
4667 def Find(self):
4668 win = self.mdi_area.activeSubWindow()
4669 if win:
4670 try:
4671 win.find_bar.Activate()
4672 except:
4673 pass
4674
4675 def FetchMoreRecords(self):
4676 win = self.mdi_area.activeSubWindow()
4677 if win:
4678 try:
4679 win.fetch_bar.Activate()
4680 except:
4681 pass
4682
4683 def ShrinkFont(self):
4684 self.Try(ShrinkFont)
4685
4686 def EnlargeFont(self):
4687 self.Try(EnlargeFont)
4688
4689 def EventMenu(self, events, reports_menu):
4690 branches_events = 0
4691 for event in events:
4692 event = event.split(":")[0]
4693 if event == "branches":
4694 branches_events += 1
4695 dbid = 0
4696 for event in events:
4697 dbid += 1
4698 event = event.split(":")[0]
4699 if event == "branches":
4700 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4701 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4702 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4703 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4704
4705 def TimeChartByCPU(self):
4706 TimeChartByCPUWindow(self.glb, self)
4707
4708 def TableMenu(self, tables, menu):
4709 table_menu = menu.addMenu("&Tables")
4710 for table in tables:
4711 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4712
4713 def NewCallGraph(self):
4714 CallGraphWindow(self.glb, self)
4715
4716 def NewCallTree(self):
4717 CallTreeWindow(self.glb, self)
4718
4719 def NewTopCalls(self):
4720 dialog = TopCallsDialog(self.glb, self)
4721 ret = dialog.exec_()
4722 if ret:
4723 TopCallsWindow(self.glb, dialog.report_vars, self)
4724
4725 def NewBranchView(self, event_id):
4726 BranchWindow(self.glb, event_id, ReportVars(), self)
4727
4728 def NewSelectedBranchView(self, event_id):
4729 dialog = SelectedBranchDialog(self.glb, self)
4730 ret = dialog.exec_()
4731 if ret:
4732 BranchWindow(self.glb, event_id, dialog.report_vars, self)
4733
4734 def NewTableView(self, table_name):
4735 TableWindow(self.glb, table_name, self)
4736
4737 def Help(self):
4738 HelpWindow(self.glb, self)
4739
4740 def About(self):
4741 dialog = AboutDialog(self.glb, self)
4742 dialog.exec_()
4743
4744# XED Disassembler
4745
4746class xed_state_t(Structure):
4747
4748 _fields_ = [
4749 ("mode", c_int),
4750 ("width", c_int)
4751 ]
4752
4753class XEDInstruction():
4754
4755 def __init__(self, libxed):
4756 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4757 xedd_t = c_byte * 512
4758 self.xedd = xedd_t()
4759 self.xedp = addressof(self.xedd)
4760 libxed.xed_decoded_inst_zero(self.xedp)
4761 self.state = xed_state_t()
4762 self.statep = addressof(self.state)
4763 # Buffer for disassembled instruction text
4764 self.buffer = create_string_buffer(256)
4765 self.bufferp = addressof(self.buffer)
4766
4767class LibXED():
4768
4769 def __init__(self):
4770 try:
4771 self.libxed = CDLL("libxed.so")
4772 except:
4773 self.libxed = None
4774 if not self.libxed:
4775 self.libxed = CDLL("/usr/local/lib/libxed.so")
4776
4777 self.xed_tables_init = self.libxed.xed_tables_init
4778 self.xed_tables_init.restype = None
4779 self.xed_tables_init.argtypes = []
4780
4781 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4782 self.xed_decoded_inst_zero.restype = None
4783 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4784
4785 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4786 self.xed_operand_values_set_mode.restype = None
4787 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4788
4789 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4790 self.xed_decoded_inst_zero_keep_mode.restype = None
4791 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4792
4793 self.xed_decode = self.libxed.xed_decode
4794 self.xed_decode.restype = c_int
4795 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4796
4797 self.xed_format_context = self.libxed.xed_format_context
4798 self.xed_format_context.restype = c_uint
4799 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4800
4801 self.xed_tables_init()
4802
4803 def Instruction(self):
4804 return XEDInstruction(self)
4805
4806 def SetMode(self, inst, mode):
4807 if mode:
4808 inst.state.mode = 4 # 32-bit
4809 inst.state.width = 4 # 4 bytes
4810 else:
4811 inst.state.mode = 1 # 64-bit
4812 inst.state.width = 8 # 8 bytes
4813 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4814
4815 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4816 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4817 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4818 if err:
4819 return 0, ""
4820 # Use AT&T mode (2), alternative is Intel (3)
4821 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4822 if not ok:
4823 return 0, ""
4824 if sys.version_info[0] == 2:
4825 result = inst.buffer.value
4826 else:
4827 result = inst.buffer.value.decode()
4828 # Return instruction length and the disassembled instruction text
4829 # For now, assume the length is in byte 166
4830 return inst.xedd[166], result
4831
4832def TryOpen(file_name):
4833 try:
4834 return open(file_name, "rb")
4835 except:
4836 return None
4837
4838def Is64Bit(f):
4839 result = sizeof(c_void_p)
4840 # ELF support only
4841 pos = f.tell()
4842 f.seek(0)
4843 header = f.read(7)
4844 f.seek(pos)
4845 magic = header[0:4]
4846 if sys.version_info[0] == 2:
4847 eclass = ord(header[4])
4848 encoding = ord(header[5])
4849 version = ord(header[6])
4850 else:
4851 eclass = header[4]
4852 encoding = header[5]
4853 version = header[6]
4854 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4855 result = True if eclass == 2 else False
4856 return result
4857
4858# Global data
4859
4860class Glb():
4861
4862 def __init__(self, dbref, db, dbname):
4863 self.dbref = dbref
4864 self.db = db
4865 self.dbname = dbname
4866 self.home_dir = os.path.expanduser("~")
4867 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4868 if self.buildid_dir:
4869 self.buildid_dir += "/.build-id/"
4870 else:
4871 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4872 self.app = None
4873 self.mainwindow = None
4874 self.instances_to_shutdown_on_exit = weakref.WeakSet()
4875 try:
4876 self.disassembler = LibXED()
4877 self.have_disassembler = True
4878 except:
4879 self.have_disassembler = False
4880 self.host_machine_id = 0
4881 self.host_start_time = 0
4882 self.host_finish_time = 0
4883
4884 def FileFromBuildId(self, build_id):
4885 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4886 return TryOpen(file_name)
4887
4888 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4889 # Assume current machine i.e. no support for virtualization
4890 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4891 file_name = os.getenv("PERF_KCORE")
4892 f = TryOpen(file_name) if file_name else None
4893 if f:
4894 return f
4895 # For now, no special handling if long_name is /proc/kcore
4896 f = TryOpen(long_name)
4897 if f:
4898 return f
4899 f = self.FileFromBuildId(build_id)
4900 if f:
4901 return f
4902 return None
4903
4904 def AddInstanceToShutdownOnExit(self, instance):
4905 self.instances_to_shutdown_on_exit.add(instance)
4906
4907 # Shutdown any background processes or threads
4908 def ShutdownInstances(self):
4909 for x in self.instances_to_shutdown_on_exit:
4910 try:
4911 x.Shutdown()
4912 except:
4913 pass
4914
4915 def GetHostMachineId(self):
4916 query = QSqlQuery(self.db)
4917 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4918 if query.next():
4919 self.host_machine_id = query.value(0)
4920 else:
4921 self.host_machine_id = 0
4922 return self.host_machine_id
4923
4924 def HostMachineId(self):
4925 if self.host_machine_id:
4926 return self.host_machine_id
4927 return self.GetHostMachineId()
4928
4929 def SelectValue(self, sql):
4930 query = QSqlQuery(self.db)
4931 try:
4932 QueryExec(query, sql)
4933 except:
4934 return None
4935 if query.next():
4936 return Decimal(query.value(0))
4937 return None
4938
4939 def SwitchesMinTime(self, machine_id):
4940 return self.SelectValue("SELECT time"
4941 " FROM context_switches"
4942 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4943 " ORDER BY id LIMIT 1")
4944
4945 def SwitchesMaxTime(self, machine_id):
4946 return self.SelectValue("SELECT time"
4947 " FROM context_switches"
4948 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4949 " ORDER BY id DESC LIMIT 1")
4950
4951 def SamplesMinTime(self, machine_id):
4952 return self.SelectValue("SELECT time"
4953 " FROM samples"
4954 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4955 " ORDER BY id LIMIT 1")
4956
4957 def SamplesMaxTime(self, machine_id):
4958 return self.SelectValue("SELECT time"
4959 " FROM samples"
4960 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4961 " ORDER BY id DESC LIMIT 1")
4962
4963 def CallsMinTime(self, machine_id):
4964 return self.SelectValue("SELECT calls.call_time"
4965 " FROM calls"
4966 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4967 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4968 " ORDER BY calls.id LIMIT 1")
4969
4970 def CallsMaxTime(self, machine_id):
4971 return self.SelectValue("SELECT calls.return_time"
4972 " FROM calls"
4973 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4974 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4975 " ORDER BY calls.return_time DESC LIMIT 1")
4976
4977 def GetStartTime(self, machine_id):
4978 t0 = self.SwitchesMinTime(machine_id)
4979 t1 = self.SamplesMinTime(machine_id)
4980 t2 = self.CallsMinTime(machine_id)
4981 if t0 is None or (not(t1 is None) and t1 < t0):
4982 t0 = t1
4983 if t0 is None or (not(t2 is None) and t2 < t0):
4984 t0 = t2
4985 return t0
4986
4987 def GetFinishTime(self, machine_id):
4988 t0 = self.SwitchesMaxTime(machine_id)
4989 t1 = self.SamplesMaxTime(machine_id)
4990 t2 = self.CallsMaxTime(machine_id)
4991 if t0 is None or (not(t1 is None) and t1 > t0):
4992 t0 = t1
4993 if t0 is None or (not(t2 is None) and t2 > t0):
4994 t0 = t2
4995 return t0
4996
4997 def HostStartTime(self):
4998 if self.host_start_time:
4999 return self.host_start_time
5000 self.host_start_time = self.GetStartTime(self.HostMachineId())
5001 return self.host_start_time
5002
5003 def HostFinishTime(self):
5004 if self.host_finish_time:
5005 return self.host_finish_time
5006 self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5007 return self.host_finish_time
5008
5009 def StartTime(self, machine_id):
5010 if machine_id == self.HostMachineId():
5011 return self.HostStartTime()
5012 return self.GetStartTime(machine_id)
5013
5014 def FinishTime(self, machine_id):
5015 if machine_id == self.HostMachineId():
5016 return self.HostFinishTime()
5017 return self.GetFinishTime(machine_id)
5018
5019# Database reference
5020
5021class DBRef():
5022
5023 def __init__(self, is_sqlite3, dbname):
5024 self.is_sqlite3 = is_sqlite3
5025 self.dbname = dbname
5026 self.TRUE = "TRUE"
5027 self.FALSE = "FALSE"
5028 # SQLite prior to version 3.23 does not support TRUE and FALSE
5029 if self.is_sqlite3:
5030 self.TRUE = "1"
5031 self.FALSE = "0"
5032
5033 def Open(self, connection_name):
5034 dbname = self.dbname
5035 if self.is_sqlite3:
5036 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5037 else:
5038 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5039 opts = dbname.split()
5040 for opt in opts:
5041 if "=" in opt:
5042 opt = opt.split("=")
5043 if opt[0] == "hostname":
5044 db.setHostName(opt[1])
5045 elif opt[0] == "port":
5046 db.setPort(int(opt[1]))
5047 elif opt[0] == "username":
5048 db.setUserName(opt[1])
5049 elif opt[0] == "password":
5050 db.setPassword(opt[1])
5051 elif opt[0] == "dbname":
5052 dbname = opt[1]
5053 else:
5054 dbname = opt
5055
5056 db.setDatabaseName(dbname)
5057 if not db.open():
5058 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5059 return db, dbname
5060
5061# Main
5062
5063def Main():
5064 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5065 " or: exported-sql-viewer.py --help-only"
5066 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5067 ap.add_argument("--pyside-version-1", action='store_true')
5068 ap.add_argument("dbname", nargs="?")
5069 ap.add_argument("--help-only", action='store_true')
5070 args = ap.parse_args()
5071
5072 if args.help_only:
5073 app = QApplication(sys.argv)
5074 mainwindow = HelpOnlyWindow()
5075 mainwindow.show()
5076 err = app.exec_()
5077 sys.exit(err)
5078
5079 dbname = args.dbname
5080 if dbname is None:
5081 ap.print_usage()
5082 print("Too few arguments")
5083 sys.exit(1)
5084
5085 is_sqlite3 = False
5086 try:
5087 f = open(dbname, "rb")
5088 if f.read(15) == b'SQLite format 3':
5089 is_sqlite3 = True
5090 f.close()
5091 except:
5092 pass
5093
5094 dbref = DBRef(is_sqlite3, dbname)
5095 db, dbname = dbref.Open("main")
5096 glb = Glb(dbref, db, dbname)
5097 app = QApplication(sys.argv)
5098 glb.app = app
5099 mainwindow = MainWindow(glb)
5100 glb.mainwindow = mainwindow
5101 mainwindow.show()
5102 err = app.exec_()
5103 glb.ShutdownInstances()
5104 db.close()
5105 sys.exit(err)
5106
5107if __name__ == "__main__":
5108 Main()