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