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