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